MaterialTheme in Jetpack Compose: Colors, Typography, and Shapes

Quick answer: Put one Material 3
MaterialThemenear the root of your Compose hierarchy. Give it a light and darkColorScheme, a sharedTypographyscale, and aShapesscale. Read those values throughMaterialTheme.colorScheme,MaterialTheme.typography, andMaterialTheme.shapesinstead 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
| System | What it controls | Read it with |
|---|---|---|
ColorScheme | Surface, text, buttons, states, and semantic status colors | MaterialTheme.colorScheme |
Typography | Named text roles, font family, weight, size, and line height | MaterialTheme.typography |
Shapes | Corner treatment for cards, buttons, sheets, and custom surfaces | MaterialTheme.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.
| Role | Use it for | Pair it with |
|---|---|---|
primary | Main actions, selected state, prominent emphasis | onPrimary |
secondary | Supporting emphasis and less prominent actions | onSecondary |
tertiary | A contrasting accent or special emphasis | onTertiary |
surface | Cards, sheets, menus, and screen surfaces | onSurface |
surfaceVariant | Lower-emphasis containers | onSurfaceVariant |
error | Error states and error actions | onError |
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
- Define complete light and dark color schemes for your brand.
- Choose whether dynamic color is appropriate and retain a fallback for Android 11 and lower.
- Use semantic
ColorSchemeroles and matchingon*content colors. - Define a small, named typography scale and a related shape scale.
- 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.