The Jetpack Compose Styles API, Explained
Compose teams hit the same wall around screen 40: Text calls with slightly different fontSize values, RoundedCornerShape(12.dp) copy-pasted with 14.dp on another screen, and a "primary button" that looks different on checkout vs settings. MaterialTheme covers Material defaults; it does not solve branded component styles or design-token governance. The Jetpack Compose Styles API arriving in Compose 1.11 (Compose BOM 2025.12+) addresses that gap — named, reusable styles you define once and apply like tokens.
I have migrated large XML-era design systems to Compose; the Styles API is the missing layer between MaterialTheme and one-off modifier chains. Here is how it works and when to adopt it.
The problem it solves
Before Styles, teams used three patterns — all leaky:
| Pattern | Pain |
|---|---|
| Raw parameters on every composable | Drift, no single source of truth |
Wrapper composables (AppButton) |
Proliferation, hard to theme per brand |
CompositionLocal for everything |
Implicit, untyped, debug nightmares |
The Styles API introduces first-class style objects registered in a hierarchy and resolved at composition time — closer to CSS classes or SwiftUI view styles than to copying TextStyle instances.
Core concepts in Compose 1.11
Style definitions
You define styles with the style DSL (API names per Compose 1.11 release notes):
// Design system module — styles/ChargePortStyles.kt
import androidx.compose.ui.styles.Style
import androidx.compose.ui.styles.style
val ChargePortStyles = Style("ChargePort") {
textStyle("headingLarge") {
fontSize = 28.sp
fontWeight = FontWeight.SemiBold
lineHeight = 36.sp
}
textStyle("bodyMuted") {
fontSize = 14.sp
color = Color(0xFF6B7280)
lineHeight = 20.sp
}
shapeStyle("card") {
shape = RoundedCornerShape(16.dp)
}
}
Applying styles
Composables that support the Styles API expose style parameters or use Modifier.style():
@Composable
fun ChargerStatusCard(
title: String,
modifier: Modifier = Modifier,
) {
Card(
modifier = modifier.style(ChargePortStyles.shapeStyle("card")),
) {
Text(
text = title,
style = ChargePortStyles.textStyle("headingLarge"),
)
}
}
The win: designers rename headingLarge in one file; every screen updates. No grep for 28.sp.
Style hierarchy and overrides
Styles compose in a parent-child tree — child styles inherit unspecified properties:
val BaseButtonStyle = Style("Button") {
textStyle("label") {
fontSize = 16.sp
fontWeight = FontWeight.Medium
}
}
val PrimaryButtonStyle = BaseButtonStyle.extend("PrimaryButton") {
textStyle("label") {
color = Color.White // overrides only color
}
// fontSize 16.sp inherited
}
This mirrors design-token trees in Figma: button/primary/label extends button/label.
Integration with MaterialTheme
MaterialTheme remains the foundation for Material 3 color roles and elevation. Styles sit beside Material, not instead of it:
@Composable
fun ChargePortTheme(
content: @Composable () -> Unit,
) {
MaterialTheme(
colorScheme = chargePortLightColors,
typography = Typography(), // Material baseline
) {
StyleProvider(ChargePortStyles) {
content()
}
}
}
Use MaterialTheme.colorScheme.primary inside style definitions for theme-aware colors:
textStyle("link") {
color = MaterialTheme.colorScheme.primary
}
Read that inside a @Composable context or use deferred color providers where the API requires composition-bound values.
For broader Compose architecture — state, recomposition, migration — see Jetpack Compose lessons from 10 years in Android.
Component styles vs text styles
The API distinguishes:
- TextStyle tokens — typography roles (
titleMedium,caption,monospaceCode) - ShapeStyle tokens — corners, outlines for cards, chips, sheets
- Component styles (where supported) — bundled foreground, background, padding for
Button,TextField
componentStyle("filledButton") {
shape = RoundedCornerShape(12.dp)
colors = ButtonColors(
containerColor = Color(0xFF0F766E),
contentColor = Color.White,
)
padding = PaddingValues(horizontal = 24.dp, vertical = 12.dp)
}
Prefer component styles when the design system specifies combinations that must not mix arbitrarily.
Multi-brand and dynamic theming
EV and fleet apps often white-label per operator. Styles enable brand packs:
fun brandStyles(operatorId: String): Style = when (operatorId) {
"op_cairo" -> CairoOperatorStyles
"op_gulf" -> GulfOperatorStyles
else -> DefaultChargePortStyles
}
@Composable
fun BrandedApp(operatorId: String, content: @Composable () -> Unit) {
StyleProvider(brandStyles(operatorId)) {
MaterialTheme(colorScheme = schemeFor(operatorId)) {
content()
}
}
}
Swap StyleProvider at the root — child composables stay unchanged. This is cleaner than if (brand == X) 14.dp else 12.dp inside UI.
Migration from ad-hoc styling
Incremental path I use on production migrations:
- Audit — static analysis or Compose compiler reports listing unique
TextStyle/ shape literals. - Tokenize top 10 — headings, body, caption, primary/secondary button, card shape.
- Wrap, do not rewrite — existing
AppButtondelegates tocomponentStyle("filledButton"). - Lint — custom detekt rule blocking raw
fontSize =outside the design system module. - Screenshot tests — one golden per style token on a reference device.
Do not big-bang migrate 200 screens. Tokenize shared components first; screens inherit on contact.
Testing and previews
Styles are composition-local state — test with explicit providers:
@Preview
@Composable
private fun ChargerCardPreview() {
StyleProvider(ChargePortStyles) {
ChargePortTheme {
ChargerStatusCard(title = "CP-204 online")
}
}
}
For JVM screenshot tests (Roborazzi, Paparazzi), pin StyleProvider in the test harness the same way you pin MaterialTheme.
Performance considerations
Style resolution happens at composition; cached style objects are cheap. Avoid creating new Style { } blocks inside recomposition — define styles as top-level vals, not inside composable bodies. Same rule as MaterialTheme.typography — allocate once.
If you pass styles through unstable data classes, mark holders @Immutable to preserve skipping — the same stability discipline from large Compose codebases.
When to wait
- Compose BOM below 2025.12 / UI below 1.11 — pin versions before adopting unstable APIs.
- No design token document — styles encode governance; without design buy-in, you recreate chaos with extra steps.
- Single-brand toy app —
MaterialThemeextensions suffice.
BOM setup
// libs.versions.toml
[versions]
composeBom = "2025.12.00"
[libraries]
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
compose-ui = { group = "androidx.compose.ui", name = "ui" }
dependencies {
implementation(platform(libs.compose.bom))
implementation(libs.compose.ui)
}
Check Compose UI release notes for API renames as Styles stabilizes — the concept is stable even if DSL names shift slightly across betas.
Summary
The Compose Styles API is design tokens for Compose: named text and shape styles, hierarchies with inheritance, and brand packs via StyleProvider. Keep MaterialTheme for Material color and type baselines; use Styles for product-specific component appearance. Define tokens in one module, lint against raw literals, and migrate through shared components — not screen-by-screen parameter hunts.
Resources
- Compose UI 1.11 Release Notes
- Jetpack Compose BOM Versions
- Material 3 in Compose
- Compose Material 3 Theming
- Android Developers — Compose Performance
- Compose Compiler Metrics Guide
Frequently asked questions
What is the Jetpack Compose Styles API?
The Styles API (stabilizing in Compose 1.11) provides a structured way to define, name, and reuse visual styles — typography, colors, shapes, and component-specific appearances — across composables, similar to design tokens in Figma tied to code.
How is the Styles API different from MaterialTheme?
MaterialTheme supplies Material Design defaults (colorScheme, typography, shapes). The Styles API lets you define custom style hierarchies and component styles beyond Material — branded buttons, cards, and text roles — and apply them consistently without passing individual parameters everywhere.
Do I need the Styles API for a small Compose app?
No. Small apps can rely on MaterialTheme and theme extensions. The Styles API pays off when you have a design system with named tokens, multiple brands, or frequent style updates that should not require hunting modifier chains across dozens of screens.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →