MaterialTheme in Jetpack Compose: Colors, Typography, and Shapes

Quick answer: Put one Material 3 MaterialTheme near the root of your Compose hierarchy. Give it a light and dark ColorScheme, a shared Typography scale, and a Shapes scale. Read those values through MaterialTheme.colorScheme, MaterialTheme.typography, and MaterialTheme.shapes instead of scattering literal colors, font sizes, and corner radii across features.

MaterialTheme is the contract between your app’s visual decisions and its Compose UI. It lets Material 3 components—and your own components—share color roles, text styles, and shapes without each screen inventing its own values.

Material 3 defines these three core theme systems: color scheme, typography, and shapes. The official Material 3 Compose guide explains that components use the values supplied by MaterialTheme as their defaults, making a well-defined theme the foundation of a consistent UI.

The three systems a theme owns

SystemWhat it controlsRead it with
ColorSchemeSurface, text, buttons, states, and semantic status colorsMaterialTheme.colorScheme
TypographyNamed text roles, font family, weight, size, and line heightMaterialTheme.typography
ShapesCorner treatment for cards, buttons, sheets, and custom surfacesMaterialTheme.shapes

Keep the theme separate from feature UI. A common project structure is ui/theme/Color.kt, Type.kt, Shape.kt, and Theme.kt; features then only consume the public values through MaterialTheme.

Create a small Material 3 theme

The following is an illustrative Theme.kt pattern. It supports the system dark-theme setting, custom fallback schemes, and dynamic color on supported devices.

import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Shapes
import androidx.compose.material3.Typography
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

private val LightColors = lightColorScheme(
    primary = Color(0xFF4F378B),
    onPrimary = Color.White,
    secondary = Color(0xFF625B71),
    onSecondary = Color.White,
    surface = Color(0xFFFFFBFE),
    onSurface = Color(0xFF1C1B1F),
)

private val DarkColors = darkColorScheme(
    primary = Color(0xFFD0BCFF),
    onPrimary = Color(0xFF381E72),
    secondary = Color(0xFFCCC2DC),
    onSecondary = Color(0xFF332D41),
    surface = Color(0xFF141218),
    onSurface = Color(0xFFE6E0E9),
)

private val AppTypography = Typography(
    headlineMedium = TextStyle(
        fontWeight = FontWeight.SemiBold,
        fontSize = 28.sp,
        lineHeight = 34.sp,
    ),
    bodyLarge = TextStyle(
        fontSize = 16.sp,
        lineHeight = 24.sp,
    ),
    labelLarge = TextStyle(
        fontWeight = FontWeight.Medium,
        fontSize = 14.sp,
    ),
)

private val AppShapes = Shapes(
    extraSmall = RoundedCornerShape(4.dp),
    small = RoundedCornerShape(8.dp),
    medium = RoundedCornerShape(16.dp),
    large = RoundedCornerShape(24.dp),
    extraLarge = RoundedCornerShape(32.dp),
)

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

    MaterialTheme(
        colorScheme = colorScheme,
        typography = AppTypography,
        shapes = AppShapes,
        content = content,
    )
}

Wrap your app content once:

setContent {
    AppTheme {
        MyApp()
    }
}

The androidx.compose.material3:material3 artifact provides these APIs. If your project manages Compose versions with the BOM, add the Material 3 artifact without a version; the Compose BOM setup guide shows that dependency pattern.

Use color roles, not a favorite color everywhere

Material 3 color names express a role, not only a hue. This makes light and dark themes work without every feature needing separate if (darkTheme) branches.

RoleUse it forPair it with
primaryMain actions, selected state, prominent emphasisonPrimary
secondarySupporting emphasis and less prominent actionsonSecondary
tertiaryA contrasting accent or special emphasisonTertiary
surfaceCards, sheets, menus, and screen surfacesonSurface
surfaceVariantLower-emphasis containersonSurfaceVariant
errorError states and error actionsonError

The on* roles are intended for content drawn over their matching background: for example, text on primary should use onPrimary. Using the role pair protects contrast decisions when the theme changes.

@Composable
fun AccountSummary(name: String) {
    Card(
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.surfaceVariant,
        ),
        shape = MaterialTheme.shapes.medium,
    ) {
        Column(Modifier.padding(20.dp)) {
            Text(
                text = "Welcome back, $name",
                style = MaterialTheme.typography.titleLarge,
                color = MaterialTheme.colorScheme.onSurfaceVariant,
            )
            Text(
                text = "Your account is ready.",
                style = MaterialTheme.typography.bodyMedium,
                color = MaterialTheme.colorScheme.onSurfaceVariant,
            )
        }
    }
}

The best default is to let Material components choose their own theme-aware colors. Pass CardDefaults, ButtonDefaults, or a direct color role only when the component needs a purposeful variant.

Add dark theme and dynamic color safely

isSystemInDarkTheme() is a sensible default because it follows the device setting. Always keep your own light and dark fallback schemes, even when dynamic color is enabled.

Dynamic color derives a color scheme from the user’s wallpaper on Android 12 (API 31) and later through dynamicLightColorScheme() and dynamicDarkColorScheme(). On older devices, the code falls back to your custom LightColors and DarkColors. This behavior is documented in the Material 3 dynamic color guidance.

Dynamic color is a product decision, not an automatic requirement. It can make an app feel native to a user’s device, but it can also conflict with a tightly controlled brand palette. Keep the dynamicColor flag so you can disable it for a white-label product, a brand-critical surface, or an A/B-tested design.

Build a type scale, not isolated text styles

Use named styles from MaterialTheme.typography to communicate hierarchy. For example:

Column(Modifier.padding(24.dp)) {
    Text(
        text = "Payment details",
        style = MaterialTheme.typography.headlineMedium,
    )
    Spacer(Modifier.height(12.dp))
    Text(
        text = "Choose a saved card or add a new one.",
        style = MaterialTheme.typography.bodyLarge,
        color = MaterialTheme.colorScheme.onSurfaceVariant,
    )
    Spacer(Modifier.height(20.dp))
    Button(onClick = onAddCard) {
        Text("Add card", style = MaterialTheme.typography.labelLarge)
    }
}

headlineMedium, bodyLarge, and labelLarge convey intent more clearly than a series of 18.sp, 16.sp, and 14.sp literals in feature code. You can tune one theme role later and update the whole app consistently. When adding a custom font, define its FontFamily in the theme’s Typography; do not set it separately in every screen.

Use shapes as a scale

The Material 3 Shapes scale gives components a consistent family of corner treatments: extraSmall, small, medium, large, and extraLarge. A card, bottom sheet, and floating action button do not need the same radius, but their radii should feel related.

Use the appropriate theme shape in custom components:

Surface(
    shape = MaterialTheme.shapes.large,
    color = MaterialTheme.colorScheme.secondaryContainer,
) {
    Text(
        text = "New for you",
        modifier = Modifier.padding(16.dp),
        color = MaterialTheme.colorScheme.onSecondaryContainer,
    )
}

Avoid treating a hard-coded RoundedCornerShape(24.dp) as a feature-level styling shortcut. If that radius represents your design language, give it a name in AppShapes and consume it through the theme.

Preview the actual theme variants

An unwrapped preview often looks different from the real app. Preview each important component within AppTheme, then add variants for light and dark colors and for dynamic color if your product enables it.

@Preview(name = "Light", showBackground = true)
@Composable
fun AccountSummaryLightPreview() {
    AppTheme(darkTheme = false, dynamicColor = false) {
        AccountSummary(name = "Ada")
    }
}

@Preview(name = "Dark", showBackground = true)
@Composable
fun AccountSummaryDarkPreview() {
    AppTheme(darkTheme = true, dynamicColor = false) {
        AccountSummary(name = "Ada")
    }
}

Disabling dynamic color in deterministic previews makes visual review repeatable. Test dynamic color on an Android 12+ device or emulator separately. The Compose Preview guide covers additional preview states, font scales, and devices.

Common theming mistakes

Hard-coding feature colors

Color(0xFF...) is appropriate in the theme definition. Outside it, a literal color often becomes a maintenance problem in dark mode and makes branding changes expensive. Prefer a semantic role such as primary, surfaceVariant, or error.

Using an on* color on the wrong background

onPrimary is not a general text color; it is intended for content over primary. Match each background role with its corresponding on* role, or use Material component defaults.

Making every component highly branded

The theme should provide a coherent default. Reserve explicit color overrides for real hierarchy or state changes, not for every card and button. A screen with too many manually tinted elements loses the value of a design system.

Forgetting to test dark mode and large text

Colors that look balanced in a light preview can be too muted or low-contrast in dark mode. A type scale that works at default font size can overflow with larger system text. Test both before considering a theme complete.

Passing colors through every feature parameter

Most components should read MaterialTheme themselves. Pass a color parameter only when it represents a meaningful caller-controlled state, such as a chart-series color or a status indicator. For other visual customization, a well-placed Modifier is often the better extension point.

A practical theme checklist

  1. Define complete light and dark color schemes for your brand.
  2. Choose whether dynamic color is appropriate and retain a fallback for Android 11 and lower.
  3. Use semantic ColorScheme roles and matching on* content colors.
  4. Define a small, named typography scale and a related shape scale.
  5. Preview important components in both themes and at larger font scales.

MaterialTheme sets visual defaults; a screen shell still needs sound layout choices. Combine the theme with Scaffold and window-inset handling so your themed components remain readable and reachable edge-to-edge.

FAQ

Does MaterialTheme automatically change every composable?

Material 3 components read the theme’s defaults. Your custom composables must read MaterialTheme.colorScheme, MaterialTheme.typography, or MaterialTheme.shapes when they need themed values.

Is dynamic color available on every Android device?

No. The dynamic color helpers require Android 12 (API 31) or later. Supply custom light and dark ColorScheme fallbacks for older devices.

Should I define a custom color for every UI element?

No. Start with Material color roles and component defaults. Add a custom semantic role only when your product needs a visual meaning that the standard scheme cannot express clearly.