Skip to main content

Compose design systems and component APIs

A design system is policy in code​

Tokens define semantic color, typography, shape, spacing, elevation, and motion. Components encode allowed product behavior and accessibility. A gallery of copied buttons is not a system.

@Immutable
data class AppSpacing(val compact: Dp = 8.dp, val normal: Dp = 16.dp, val roomy: Dp = 24.dp)

val LocalAppSpacing = staticCompositionLocalOf { AppSpacing() }

@Composable
fun AppTheme(content: @Composable () -> Unit) {
CompositionLocalProvider(LocalAppSpacing provides AppSpacing()) {
MaterialTheme(colorScheme = appColors(), typography = appTypography(), content = content)
}
}

Use semantic names (criticalContainer, onCriticalContainer) rather than raw red500; semantics survive brand and dark-theme changes.

Component APIs should prevent invalid states​

❌ Boolean explosion permits contradictory states:

ActionButton(loading = true, enabled = true, destructive = true, secondary = true)

✅ Model a controlled state/variant:

enum class ActionStyle { Primary, Secondary, Destructive }

@Composable
fun AppActionButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
style: ActionStyle = ActionStyle.Primary,
state: ActionState = ActionState.Ready,
leadingIcon: (@Composable () -> Unit)? = null,
)

The component decides enabled/loading semantics consistently.

Slots preserve composition​

Slots accept product content without forcing a giant configuration object:

@Composable
fun AppCard(
title: @Composable () -> Unit,
modifier: Modifier = Modifier,
supporting: (@Composable () -> Unit)? = null,
action: (@Composable () -> Unit)? = null,
) { /* policy + layout */ }

Keep modifier near the start, apply it to the outermost node, provide sensible defaults, and avoid hidden navigation/network behavior.

Governance and evolution​

  • Define owners and contribution review.
  • Version tokens/components and publish migration notes.
  • Test light/dark, dynamic color, large font, RTL, disabled/loading/error states.
  • Use screenshot tests for visual contract and semantics tests for behavior.
  • Track exceptions; repeated overrides indicate a missing token/variant.

Do not wrap every Material component. Wrap where product policy, branding, analytics, or accessibility consistency is valuable; direct Material usage can remain for primitives with no product-specific contract.

Architect takeaway

The design system is an internal platform. Its API compatibility and migration cost affect every feature team, so treat it like a product.

Interview exercises​

  1. Design a button API for loading, icon, destructive, and disabled states.
  2. Decide which Material components your company should wrap.
  3. Plan a color-token migration across 30 feature modules.