compose-slot-api-pattern
Compose: slot API pattern
Core principle
A reusable Compose component's job is to lay things out, not to enumerate what it lays out. The moment you write title: String, subtitle: String?, leadingIcon: ImageVector?, trailingIcon: ImageVector?, trailingText: String?, showSwitch: Boolean, switchValue: Boolean, onSwitchChange: (Boolean) -> Unit?, badge: String?, …, the component has stopped describing a layout and started enumerating call sites — and the next call site will need a parameter the component doesn't have.
The fix is to delegate content to the caller via @Composable lambda parameters. The component contributes structure (where the leading bit, headline, supporting bit, trailing bit go). The caller contributes everything that goes in those slots.
Material 3's ListItem is the canonical example: every visual piece is a slot (headlineContent, supportingContent, leadingContent, trailingContent, overlineContent), not a primitive. That's not over-engineering — it's the design that scales to every list-item shape the design system needs without ever editing ListItem again.
When to use this skill
You're designing or reviewing a Compose component intended for reuse (more than one call site, now or planned), its visual content varies by caller, and any of these is true:
- Its signature has
title: String,icon: ImageVector,actionText: String?, etc. — primitive types describing content. - It has multiple optional-content parameters that vary by call site (
subtitle: String?,leadingIcon: ImageVector?,trailingText: String?). - It has boolean flags whose only purpose is to switch between content shapes (
showChevron: Boolean,showSwitch: Boolean,mode: Mode.Text | Mode.Switch | …). - It accepts a
Stringparameter where one caller would want aTextwith custom style, a second caller aTextwith aBadge, a third caller a row of icons. - It already has one slot (often
trailingorcontent) and the rest of the parameters are still primitives.