Typography Customization
Just like ColorScheme, Compose's Typography is a set of named,
semantic text styles rather than a single font size you sprinkle around by hand. The previous lesson
passed a default RecipeBoxTypography into MaterialTheme without explaining
it — this lesson builds that value properly, giving RecipeBox's recipe titles, ingredient lists and
labels a consistent, deliberate type scale.
The Typography Data Class
Typography holds fifteen named TextStyle slots, grouped into five roles at
three sizes each — display, headline, title, body, and label, each with a large/medium/small variant.
Every Material 3 component picks one of these automatically: Text defaults to
bodyLarge, a Button's label uses labelLarge, a
TopAppBar title uses titleLarge.
| Style | Typical use in RecipeBox |
|---|---|
displayLarge | Rarely used — a huge hero number, like a "12 recipes" stat on a profile screen |
headlineMedium | A recipe's title at the top of the Recipe Detail screen |
titleMedium | Each recipe's title inside a list card |
bodyLarge | Ingredient lines and step instructions — the bulk of reading text |
labelSmall | The category chip and "Prep: 20 min" caption text |
Customizing RecipeBox's Type Scale
Typography() is a builder just like lightColorScheme() — call it and
override only the styles you want to change; every style you skip falls back to Material 3's default.
RecipeBox wants slightly bolder headings to make recipe titles stand out while scrolling a long list:
Theme/Type.kt
val RecipeBoxTypography = Typography(
headlineMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Bold,
fontSize = 28.sp,
lineHeight = 34.sp
),
titleMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.SemiBold,
fontSize = 18.sp,
lineHeight = 24.sp
),
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 22.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
letterSpacing = 0.5.sp
)
)
Every unlisted style — displayLarge, headlineLarge, bodySmall,
and so on — keeps its Material 3 default automatically, so there's no need to redefine all fifteen
just to change four.
Loading a Custom Font Family
FontFamily.Default uses the system font (Roboto on most devices). Dropping in a brand
font means placing .ttf/.otf files under res/font/ and building
a FontFamily that maps each file to its weight:
Theme/Type.kt — a custom font family
val PoppinsFamily = FontFamily(
Font(R.font.poppins_regular, FontWeight.Normal),
Font(R.font.poppins_medium, FontWeight.Medium),
Font(R.font.poppins_semibold, FontWeight.SemiBold),
Font(R.font.poppins_bold, FontWeight.Bold)
)
Swap fontFamily = FontFamily.Default for fontFamily = PoppinsFamily in any
of the TextStyle overrides above and every composable using that role — every recipe
title, every screen title — switches font automatically, with no changes anywhere else in the app.
Using Typography In a Composable
Exactly like colors, text styles are read through MaterialTheme.typography, never
constructed inline with a raw fontSize:
RecipeDetailHeader.kt
@Composable
fun RecipeDetailHeader(title: String, category: String, prepMinutes: Int) {
Column {
Text(text = title, style = MaterialTheme.typography.headlineMedium)
Text(
text = "$category • $prepMinutes min",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.secondary
)
}
}
MaterialTheme.typography with a role from MaterialTheme.colorScheme in the same Text — as above — is the normal pattern: type and color are two independent theme axes that compose together.