Theming & Styling

Jetpack Compose theming — Material 3 color schemes, typography, shapes, custom themes, and dark mode

Compose theming with Material 3 is built on three pillars: color, typography, and shape. Here’s how to configure and customize them.

Color scheme

// Light scheme
private val LightColorScheme = lightColorScheme(
    primary = Color(0xFF6750A4),
    onPrimary = Color(0xFFFFFFFF),
    primaryContainer = Color(0xFFEADDFF),
    onPrimaryContainer = Color(0xFF21005D),
    secondary = Color(0xFF625B71),
    onSecondary = Color(0xFFFFFFFF),
    secondaryContainer = Color(0xFFE8DEF8),
    onSecondaryContainer = Color(0xFF1D192B),
    tertiary = Color(0xFF7D5260),
    onTertiary = Color(0xFFFFFFFF),
    tertiaryContainer = Color(0xFFFFD8E4),
    onTertiaryContainer = Color(0xFF31111D),
    error = Color(0xFFB3261E),
    onError = Color(0xFFFFFFFF),
    errorContainer = Color(0xFFF9DEDC),
    onErrorContainer = Color(0xFF410E0B),
    background = Color(0xFFFFFBFE),
    onBackground = Color(0xFF1C1B1F),
    surface = Color(0xFFFFFBFE),
    onSurface = Color(0xFF1C1B1F),
    surfaceVariant = Color(0xFFE7E0EC),
    onSurfaceVariant = Color(0xFF49454F),
    outline = Color(0xFF79747E),
    outlineVariant = Color(0xFFCAC4D0),
)

// Dark scheme
private val DarkColorScheme = darkColorScheme(
    primary = Color(0xFFD0BCFF),
    onPrimary = Color(0xFF381E72),
    primaryContainer = Color(0xFF4F378B),
    onPrimaryContainer = Color(0xFFEADDFF),
    secondary = Color(0xFFCCC2DC),
    onSecondary = Color(0xFF332D41),
    secondaryContainer = Color(0xFF4A4458),
    onSecondaryContainer = Color(0xFFE8DEF8),
    tertiary = Color(0xFFEFB8C8),
    onTertiary = Color(0xFF492532),
    tertiaryContainer = Color(0xFF633B48),
    onTertiaryContainer = Color(0xFFFFD8E4),
    error = Color(0xFFF2B8B5),
    onError = Color(0xFF601410),
    errorContainer = Color(0xFF8C1D18),
    onErrorContainer = Color(0xFFF9DEDC),
    background = Color(0xFF1C1B1F),
    onBackground = Color(0xFFE6E1E5),
    surface = Color(0xFF1C1B1F),
    onSurface = Color(0xFFE6E1E5),
    surfaceVariant = Color(0xFF49454F),
    onSurfaceVariant = Color(0xFFCAC4D0),
    outline = Color(0xFF938F99),
    outlineVariant = Color(0xFF49454F),
)

Dynamic color (Android 12+)

@Composable
fun MyTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit,
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context)
            else dynamicLightColorScheme(context)
        }
        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        shapes = Shapes,
        content = content,
    )
}

Using theme colors

// Access colors from the theme
val colors = MaterialTheme.colorScheme

Text(
    text = "Hello",
    color = colors.onSurface,
)

Surface(
    color = colors.surface,
    contentColor = colors.onSurface,
) {
    Text("Content inherits onSurface color")
}

// Use semantic color roles
Button(
    colors = ButtonDefaults.buttonColors(
        containerColor = colors.primary,
        contentColor = colors.onPrimary,
    )
) { Text("Primary action") }

OutlinedButton(
    colors = ButtonDefaults.outlinedButtonColors(
        contentColor = colors.primary,
    )
) { Text("Secondary action") }

// Surface color variants
Surface(color = colors.surfaceVariant) {
    Text("Subtle background", color = colors.onSurfaceVariant)
}

Typography

val Typography = Typography(
    displayLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 57.sp,
        lineHeight = 64.sp,
        letterSpacing = (-0.25).sp,
    ),
    displayMedium = TextStyle(fontSize = 45.sp, lineHeight = 52.sp),
    displaySmall = TextStyle(fontSize = 36.sp, lineHeight = 44.sp),
    headlineLarge = TextStyle(fontSize = 32.sp, lineHeight = 40.sp),
    headlineMedium = TextStyle(fontSize = 28.sp, lineHeight = 36.sp),
    headlineSmall = TextStyle(fontSize = 24.sp, lineHeight = 32.sp),
    titleLarge = TextStyle(fontSize = 22.sp, lineHeight = 28.sp, fontWeight = FontWeight.Medium),
    titleMedium = TextStyle(fontSize = 16.sp, lineHeight = 24.sp, fontWeight = FontWeight.Medium),
    titleSmall = TextStyle(fontSize = 14.sp, lineHeight = 20.sp, fontWeight = FontWeight.Medium),
    bodyLarge = TextStyle(fontSize = 16.sp, lineHeight = 24.sp, letterSpacing = 0.5.sp),
    bodyMedium = TextStyle(fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.25.sp),
    bodySmall = TextStyle(fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.4.sp),
    labelLarge = TextStyle(fontSize = 14.sp, lineHeight = 20.sp, fontWeight = FontWeight.Medium, letterSpacing = 0.1.sp),
    labelMedium = TextStyle(fontSize = 12.sp, lineHeight = 16.sp, fontWeight = FontWeight.Medium, letterSpacing = 0.5.sp),
    labelSmall = TextStyle(fontSize = 11.sp, lineHeight = 16.sp, fontWeight = FontWeight.Medium, letterSpacing = 0.5.sp),
)

// Usage
Text("Headline", style = MaterialTheme.typography.headlineMedium)
Text("Body", style = MaterialTheme.typography.bodyLarge)
Text("Caption", style = MaterialTheme.typography.labelSmall)

Custom fonts

// Define font family
val MyFonts = FontFamily(
    Font(R.font.my_font_regular, FontWeight.Normal),
    Font(R.font.my_font_medium, FontWeight.Medium),
    Font(R.font.my_font_bold, FontWeight.Bold),
)

// Use in typography
val Typography = Typography(
    bodyLarge = TextStyle(
        fontFamily = MyFonts,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
    ),
    headlineMedium = TextStyle(
        fontFamily = MyFonts,
        fontWeight = FontWeight.Bold,
        fontSize = 28.sp,
    ),
)

Shapes

val Shapes = Shapes(
    small = RoundedCornerShape(4.dp),     // chips, text fields
    medium = RoundedCornerShape(8.dp),     // cards, dialogs
    large = RoundedCornerShape(16.dp),     // bottom sheets, FABs
    extraLarge = RoundedCornerShape(28.dp), // modal bottom sheets
)

// Usage — MaterialTheme.shapes is automatically applied
Card { /* uses medium shape */ }
Button({ /* ... */ }) { /* uses small shape */ }

// Override per component
Card(shape = RoundedCornerShape(24.dp)) { /* custom shape */ }

Custom component styling with CompositionLocal

// Define custom component defaults
@Composable
fun MyButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    colors: ButtonColors = MyButtonDefaults.colors(),
    content: @Composable RowScope.() -> Unit,
) {
    Button(
        onClick = onClick,
        modifier = modifier,
        enabled = enabled,
        colors = colors,
        content = content,
    )
}

object MyButtonDefaults {
    @Composable
    fun colors(
        containerColor: Color = MaterialTheme.colorScheme.primary,
        contentColor: Color = MaterialTheme.colorScheme.onPrimary,
    ) = ButtonDefaults.buttonColors(
        containerColor = containerColor,
        contentColor = contentColor,
    )
}

Elevation

// Surface with elevation
Surface(
    tonalElevation = 2.dp,    // color tint shift (M3)
    shadowElevation = 4.dp,    // actual shadow
    shape = RoundedCornerShape(12.dp),
) {
    Text("Elevated surface")
}

// Card with elevation
ElevatedCard(
    elevation = CardDefaults.elevatedCardElevation(
        defaultElevation = 4.dp,
    )
) {
    Text("Elevated card")
}