Why Your App Needs a Design System
Design Tokens: The Foundation
// Custom color tokens beyond Material 3
@Immutable
data class ExtendedColors(
val success: Color,
val warning: Color,
val info: Color,
val onSuccess: Color,
val onWarning: Color,
val onInfo: Color,
val surfaceVariantDim: Color,
val borderSubtle: Color,
)
val LocalExtendedColors = staticCompositionLocalOf {
ExtendedColors(
success = Color(0xFF2E7D32),
warning = Color(0xFFF9A825),
info = Color(0xFF1565C0),
onSuccess = Color.White,
onWarning = Color.Black,
onInfo = Color.White,
surfaceVariantDim = Color(0xFF1A1C1E),
borderSubtle = Color(0xFF2C2F33),
)
}
// Spacing tokens using an 8-point grid
object Spacing {
val xxxs = 2.dp // Fine adjustment
val xxs = 4.dp
val xs = 8.dp
val sm = 12.dp
val md = 16.dp
val lg = 24.dp
val xl = 32.dp
val xxl = 48.dp
val xxxl = 64.dp
}Building the Theme Provider
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (darkTheme) darkColorScheme(
primary = Color(0xFFBB86FC),
secondary = Color(0xFF03DAC5),
surface = Color(0xFF121212),
) else lightColorScheme(
primary = Color(0xFF6200EE),
secondary = Color(0xFF03DAC5),
surface = Color(0xFFFFFBFE),
)
val extendedColors = if (darkTheme) ExtendedColors(
success = Color(0xFF66BB6A),
warning = Color(0xFFFFCA28),
info = Color(0xFF42A5F5),
onSuccess = Color.Black,
onWarning = Color.Black,
onInfo = Color.Black,
surfaceVariantDim = Color(0xFF1A1C1E),
borderSubtle = Color(0xFF2C2F33),
) else ExtendedColors(
success = Color(0xFF2E7D32),
warning = Color(0xFFF9A825),
info = Color(0xFF1565C0),
onSuccess = Color.White,
onWarning = Color.Black,
onInfo = Color.White,
surfaceVariantDim = Color(0xFFF5F5F5),
borderSubtle = Color(0xFFE0E0E0),
)
CompositionLocalProvider(
LocalExtendedColors provides extendedColors
) {
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
shapes = AppShapes,
content = content,
)
}
}
// Convenient accessor
object AppTheme {
val extendedColors: ExtendedColors
@Composable get() = LocalExtendedColors.current
}Component Library: Atoms to Organisms
// Atom: Button with consistent design language
@Composable
fun AppButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
variant: ButtonVariant = ButtonVariant.Primary,
size: ButtonSize = ButtonSize.Medium,
enabled: Boolean = true,
leadingIcon: ImageVector? = null,
) {
val colors = when (variant) {
ButtonVariant.Primary -> ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
)
ButtonVariant.Secondary -> ButtonDefaults.outlinedButtonColors()
ButtonVariant.Danger -> ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error,
contentColor = MaterialTheme.colorScheme.onError,
)
}
val padding = when (size) {
ButtonSize.Small -> PaddingValues(
horizontal = Spacing.sm, vertical = Spacing.xxs
)
ButtonSize.Medium -> PaddingValues(
horizontal = Spacing.md, vertical = Spacing.xs
)
ButtonSize.Large -> PaddingValues(
horizontal = Spacing.lg, vertical = Spacing.sm
)
}
Button(
onClick = onClick,
modifier = modifier,
enabled = enabled,
colors = colors,
contentPadding = padding,
shape = AppShapes.medium,
) {
if (leadingIcon != null) {
Icon(
imageVector = leadingIcon,
contentDescription = null,
modifier = Modifier.size(18.dp),
)
Spacer(Modifier.width(Spacing.xs))
}
Text(text = text)
}
}
enum class ButtonVariant { Primary, Secondary, Danger }
enum class ButtonSize { Small, Medium, Large }Previews and Documentation
@Preview(name = "Primary Button", group = "Buttons")
@Preview(
name = "Primary Button - Dark",
group = "Buttons",
uiMode = Configuration.UI_MODE_NIGHT_YES
)
@Composable
private fun PrimaryButtonPreview() {
AppTheme {
AppButton(
text = "Get Started",
onClick = {},
leadingIcon = Icons.Default.ArrowForward,
)
}
}
@Preview(name = "All Button Variants", group = "Buttons")
@Composable
private fun AllButtonsPreview() {
AppTheme {
Column(
verticalArrangement = Arrangement.spacedBy(Spacing.sm)
) {
ButtonVariant.entries.forEach { variant ->
ButtonSize.entries.forEach { size ->
AppButton(
text = "${variant.name} ${size.name}",
onClick = {},
variant = variant,
size = size,
)
}
}
}
}
}Key Takeaways
- 1Design tokens (colors, spacing, typography) are the atomic foundation of every component.
- 2Extend Material 3 with custom CompositionLocal tokens for app-specific needs.
- 3Follow atomic design: atoms, molecules, organisms for a scalable component library.
- 4Never hardcode values in components -- always reference design tokens.
- 5Compose Previews are living documentation for your design system.
Frequently Asked
What are design tokens in a Compose design system?
Tokens are the atomic values -- colors, spacing, typography -- that every component reads from. They are the foundation layer: components reference tokens and never hardcode a value.
How do I extend Material 3 with my own tokens?
Add custom CompositionLocal tokens alongside the Material 3 theme for app-specific needs, rather than forking or fighting the Material theme.
How should I structure a Compose component library?
Follow atomic design -- atoms, then molecules, then organisms -- so the library scales. Compose Previews then act as living documentation for each level.
Ready to architect your next Android app?
ANDROID-ARCHITECT generates production-ready Kotlin code, architecture blueprints, and CI/CD configurations from plain-language descriptions. Start building for free.