Composability patterns
Agent skills for building and maintaining mobile design systems, tokens, and component libraries.
npx -y skills add almasumdev/awesome-mobile-design-system-agent-skills --skill composability-patternsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Slots, compound components, and headless primitives for flexible mobile component APIs. Use this when a component needs to serve many callers without becoming a grab-bag of props.
SKILL.md
5.7 KB, as published. Nobody here has run it
Composability Patterns
Instructions
Props scale poorly. A component with 30 props is impossible to evolve. Three patterns let you ship small, stable APIs that still bend to every caller: slots, compound components, and headless primitives.
1. Slots
A slot is a child parameter that the caller fills with arbitrary content. Prefer slots over props whose only purpose is to render something inside the component.
Compose:
@Composable
fun Card(
modifier: Modifier = Modifier,
header: @Composable (() -> Unit)? = null,
footer: @Composable (() -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit,
) { /* layout */ }
SwiftUI:
public struct DSCard<Header: View, Content: View, Footer: View>: View {
@ViewBuilder let header: () -> Header
@ViewBuilder let content: () -> Content
@ViewBuilder let footer: () -> Footer
public var body: some View { /* layout */ }
}
Flutter:
class DsCard extends StatelessWidget {
const DsCard({super.key, this.header, required this.child, this.footer});
final Widget? header;
final Widget child;
final Widget? footer;
}
React Native:
type CardProps = { header?: ReactNode; footer?: ReactNode; children: ReactNode };
2. Compound Components
When parts of a component must share state (open/closed, selected tab, focused field), expose them as sub-components that read from a shared context. The parent owns state; children are dumb consumers.
Compose:
class TabsState internal constructor(val selected: String, val onSelect: (String) -> Unit)
val LocalTabsState = compositionLocalOf<TabsState> { error("Tabs.Root required") }
@Composable
fun TabsRoot(selected: String, onSelect: (String) -> Unit, content: @Composable () -> Unit) {
CompositionLocalProvider(LocalTabsState provides TabsState(selected, onSelect)) { content() }
}
@Composable
fun TabsList(content: @Composable RowScope.() -> Unit) { Row { content() } }
@Composable
fun TabsTrigger(value: String, label: String) {
val state = LocalTabsState.current
Button(onClick = { state.onSelect(value) }, label = label,
intent = if (state.selected == value) ButtonIntent.Primary else ButtonIntent.Secondary)
}
Usage:
TabsRoot(selected = tab, onSelect = { tab = it }) {
TabsList {
TabsTrigger("overview", "Overview")
TabsTrigger("billing", "Billing")
}
}
SwiftUI uses EnvironmentKey, Flutter uses InheritedWidget, React Native uses React Context.
3. Headless Primitives
A headless component ships behavior without styling: focus, keyboard handling, state machine. The design system wraps it with styled variants. This is how you avoid reimplementing combobox, disclosure, dialog, and tooltip correctly on four platforms.
Pattern:
// Headless: no visuals, just state & a11y contract.
class DisclosureState(initialOpen: Boolean = false) {
var isOpen by mutableStateOf(initialOpen)
private set
fun toggle() { isOpen = !isOpen }
}
@Composable
fun Disclosure(state: DisclosureState, trigger: @Composable () -> Unit, panel: @Composable () -> Unit) {
Column(Modifier.semantics { expanded = state.isOpen }) {
Box(Modifier.clickable(onClick = state::toggle)) { trigger() }
if (state.isOpen) panel()
}
}
Then a styled wrapper supplies visuals and tokens:
@Composable
fun Accordion(title: String, content: @Composable () -> Unit) {
val state = remember { DisclosureState() }
Disclosure(
state = state,
trigger = { AccordionHeader(title = title, open = state.isOpen) },
panel = { AccordionBody { content() } },
)
}
4. When to Pick Which
| Need | Pattern |
|---|---|
| Caller supplies a chunk of content into a known position | Slot |
| Sub-parts need shared state or a required assembly order | Compound |
| Behavior is reusable across many visual treatments | Headless |
| Quick one-liner customization (color, size) | Variant prop |
5. Avoid children Free-For-All
Slots should be named. children inside a Card with implicit ordering creates fragile layouts. Prefer header, media, content, footer as separate slots.
6. Escape Hatches
Every composable primitive should expose a modifier / style passthrough so callers can layer one-off tweaks without forking the component. Never allow overriding a token-bound property (e.g., backgroundColor) — forcing that through a variant prop preserves semantics.
7. Anti-Patterns
- Dozens of
render*props (renderHeader,renderFooter) instead of slots. - Sub-components usable outside their parent with silent no-op behavior.
- A headless hook that leaks visual tokens (colors, paddings).
- A modifier/style prop that lets callers bypass semantic tokens.
Checklist
- Content injection uses named slots rather than
render*props. - Multi-part components share state via context/composition-local/environment — parent owns it.
- Complex behavior (menu, dialog, combobox) lives in a headless primitive, styled by a wrapper.
- Every component exposes a
modifier/stylepassthrough for layout-only tweaks. - Compound sub-components fail loudly outside their parent.
- No styling leaks into headless primitives.