MaterialTheme & Color Schemes
Every screen we've built for RecipeBox so far has been using Material 3's default
purple color scheme, because every one of those composables sits inside a MaterialTheme { }
wrapper that Android Studio generated for us. This lesson is about taking control of that wrapper —
understanding exactly what MaterialTheme provides to everything inside it, what a
ColorScheme's semantic roles actually mean, and building RecipeBox its own warm, food-app-appropriate
color identity instead of the generic template purple.
What MaterialTheme Actually Provides
MaterialTheme is a composable function that takes three pieces of design data —
colorScheme, typography, and shapes — and makes them available
to every composable nested inside its content lambda, no matter how deep. It does this
through CompositionLocal under the hood, which is why you can call
MaterialTheme.colorScheme.primary from any composable in the tree without passing colors
down as parameters manually.
The MaterialTheme signature (simplified)
@Composable
fun MaterialTheme(
colorScheme: ColorScheme = MaterialTheme.colorScheme,
typography: Typography = MaterialTheme.typography,
shapes: Shapes = MaterialTheme.shapes,
content: @Composable () -> Unit
)
Every Material 3 component — Button, Card, TopAppBar,
TextField — reads its default colors and text styles straight from whichever
MaterialTheme wraps it. Change the colorScheme passed at the top of the app,
and every one of those components updates everywhere, with zero changes to the screens themselves.
ColorScheme: Semantic Roles, Not Raw Hex Values
The single most important habit this lesson teaches is that a ColorScheme is made of
roles, not colors. You never write Color(0xFFEF6C00) inside a composable
to color a button — you write MaterialTheme.colorScheme.primary, and you decide what hex
value primary actually is exactly once, in the theme setup. The role name describes the
color's purpose; only the theme definition decides its actual value.
| Role | Meaning |
|---|---|
primary | The app's main brand color — FABs, filled buttons, active states |
onPrimary | Content (usually text/icons) drawn on top of a primary surface |
secondary | A supporting accent color for less prominent components (filter chips, secondary actions) |
surface | The background color of components like Card and Sheet |
background | The color behind scrollable content — the screen's base color |
error | Reserved for error states — invalid form fields, destructive-action confirmations |
onSurface / onBackground | Default text/icon color on top of surface / background |
Because components always reference roles, swapping primary from orange to green never
requires touching a single screen — every composable that asked for "the primary color" simply
receives the new value the next time it recomposes.
Building RecipeBox's Light ColorScheme
lightColorScheme() is a builder function that returns a complete ColorScheme,
filling in any role you don't specify with Material 3's sensible defaults. RecipeBox is a food app, so
a warm orange makes a much better primary than the default purple:
Theme/Color.kt
val RecipeOrange = Color(0xFFEF6C00)
val RecipeOrangeLight = Color(0xFFFFB74D)
val RecipeGreen = Color(0xFF558B2F)
val RecipeCream = Color(0xFFFFF8F0)
val RecipeErrorRed = Color(0xFFBA1A1A)
val RecipeBoxLightColors = lightColorScheme(
primary = RecipeOrange,
onPrimary = Color.White,
secondary = RecipeGreen,
onSecondary = Color.White,
background = RecipeCream,
onBackground = Color(0xFF1B1B1B),
surface = Color.White,
onSurface = Color(0xFF1B1B1B),
error = RecipeErrorRed,
onError = Color.White
)
Building RecipeBox's Dark ColorScheme
darkColorScheme() follows the same shape, but dark themes need more than just inverting
light and dark — a saturated orange that looks great on a cream background can be harsh on near-black
surfaces, so dark schemes usually lighten and slightly desaturate the primary color instead:
Theme/Color.kt (continued)
val RecipeBoxDarkColors = darkColorScheme(
primary = RecipeOrangeLight,
onPrimary = Color(0xFF4A2800),
secondary = Color(0xFFA5D6A7),
onSecondary = Color(0xFF1B3200),
background = Color(0xFF121212),
onBackground = Color(0xFFECECEC),
surface = Color(0xFF1E1E1E),
onSurface = Color(0xFFECECEC),
error = Color(0xFFFFB4AB),
onError = Color(0xFF690005)
)
Wiring the ColorScheme into RecipeBoxTheme
These two ColorScheme values are only useful once something decides which one to use and
passes it into MaterialTheme. RecipeBox wraps that decision in its own
RecipeBoxTheme composable, which every screen calls at its root:
Theme/Theme.kt
@Composable
fun RecipeBoxTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (darkTheme) RecipeBoxDarkColors else RecipeBoxLightColors
MaterialTheme(
colorScheme = colorScheme,
typography = RecipeBoxTypography,
content = content
)
}
RecipeBoxTypography is built in the next lesson — for now, know that MaterialTheme always takes a Typography alongside the ColorScheme, even a default one.Using Roles Instead of Hardcoded Colors
With RecipeBoxTheme wrapping RecipeApp() once at the top level, every
composable inside reads colors through MaterialTheme.colorScheme — never through a
literal Color(...) value:
RecipeListItem.kt — correct vs. wrong
// Correct: reads the current theme's role
@Composable
fun RecipeListItem(title: String, category: String) {
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
)
) {
Text(
text = title,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = category,
color = MaterialTheme.colorScheme.secondary
)
}
}
// Wrong: hardcoded hex ignores dark mode and any future re-theme
Text(text = title, color = Color(0xFF1B1B1B))
The hardcoded version happens to look fine in light mode, but flip to RecipeBoxDarkColors
and that near-black text sits on a near-black card — invisible. Reading through
MaterialTheme.colorScheme means RecipeListItem never needs to know or care
which theme is active; it just asks for the role it needs.
MaterialTheme also carries a Typography object — Typography Customization covers giving RecipeBox its own named text styles.