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.

StyleTypical use in RecipeBox
displayLargeRarely used — a huge hero number, like a "12 recipes" stat on a profile screen
headlineMediumA recipe's title at the top of the Recipe Detail screen
titleMediumEach recipe's title inside a list card
bodyLargeIngredient lines and step instructions — the bulk of reading text
labelSmallThe 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
        )
    }
}
Note: Combining a role from 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.
Next up: Colors and type scale both need to adapt when the device switches appearance — Dark Mode in Compose covers exactly that switch.