How to Implement Material 3 Theming with Custom Colors and Typography in Jetpack Compose

Material 3 theming in Jetpack Compose works by defining custom Color objects in Color.kt, constructing lightColorScheme and darkColorScheme instances in Theme.kt, and passing a custom Typography object from Type.kt into the MaterialTheme composable, which propagates these design tokens to all descendant composables via MaterialTheme.colorScheme and MaterialTheme.typography.

The carguo/gsygithubappcompose repository demonstrates a production-ready implementation of Material Design 3 in a GitHub client application. By isolating color definitions, typography scales, and theme logic into dedicated files, the app maintains consistent visual branding across light and dark modes while supporting dynamic color capabilities on Android 12 and above.

Defining Custom Color Palettes in Color.kt

The foundation of the theme begins with raw color constants defined in core/ui/src/main/java/com/shuyu/gsygithubappcompose/core/ui/theme/Color.kt. These hexadecimal values represent the brand palette before they are assigned semantic roles.

Creating Color Constants

Define immutable Color objects for your brand colors, neutral surfaces, and text colors:

// core/ui/src/main/java/com/shuyu/gsygithubappcompose/core/ui/theme/Color.kt
val PrimaryColor   = Color(0xFF24292E)   // main brand gray
val PrimaryLight  = Color(0xFF444D56)
val PrimaryDark   = Color(0xFF1B1F23)

val SecondaryColor = Color(0xFFADB0B6)
val SecondaryLight = Color(0xFFA3A3A8)

val BackgroundLight = Color(0xFFFAFBFC)
val BackgroundDark  = Color(0xFF24292E)

Building Light and Dark Color Schemes

In core/ui/src/main/java/com/shuyu/gsygithubappcompose/core/ui/theme/Theme.kt, these raw colors are mapped to Material 3 semantic roles using lightColorScheme() and darkColorScheme():

// Theme.kt
private val DarkColorScheme = darkColorScheme(
    primary = PrimaryColor,
    secondary = SecondaryColor,
    tertiary = SecondaryLight,
    background = BackgroundDark,
    surface = SurfaceDark,
    onPrimary = TextOnDark,
    onBackground = TextPrimary
)

private val LightColorScheme = lightColorScheme(
    primary = PrimaryColor,
    secondary = SecondaryColor,
    tertiary = SecondaryLight,
    background = BackgroundLight,
    surface = SurfaceLight,
    onPrimary = TextOnDark,
    onBackground = TextPrimary
)

Configuring Custom Typography in Type.kt

Material 3 theming allows you to override specific text styles without redefining the entire type scale. The gsygithubappcompose project places these definitions in app/src/main/java/com/shuyu/gsygithubappcompose/ui/theme/Type.kt.

Customizing Text Styles

Instantiate the Typography class and override only the slots you need. Unspecified slots automatically fall back to Material defaults:

// Type.kt
val Typography = Typography(
    bodyLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.5.sp
    )
    // Override other slots like titleLarge, labelSmall as needed
)

Integrating Typography with MaterialTheme

The custom Typography instance is injected into the theme hierarchy alongside the color scheme:

// Theme.kt
MaterialTheme(
    colorScheme = colorScheme,
    typography = Typography,
    content = content
)

Implementing the Theme Wrapper in Theme.kt

The GSYGithubAppComposeTheme composable serves as the single entry point for applying your custom Material 3 theming across the application.

The GSYGithubAppComposeTheme Composable

This function selects the appropriate color scheme based on system settings and wraps the content with MaterialTheme:

@Composable
fun GSYGithubAppComposeTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = false, // Disabled to preserve brand colors
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) 
            else dynamicLightColorScheme(context)
        }
        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

Supporting Light, Dark, and Dynamic Color Modes

The theme supports three distinct modes:

  • Dynamic colors: Extracted from the user's wallpaper on Android 12+ (disabled by default in this repository)
  • System dark mode: Uses DarkColorScheme with custom brand grays
  • System light mode: Uses LightColorScheme with the same brand palette

Consuming Theme Values in UI Components

All UI composables access the theme through MaterialTheme static properties, ensuring visual consistency without hardcoding values.

Referencing Colors with MaterialTheme.colorScheme

Components read semantic color roles rather than raw hex values:

Button(
    onClick = { /*...*/ },
    colors = ButtonDefaults.buttonColors(
        containerColor = MaterialTheme.colorScheme.primary,
        contentColor = MaterialTheme.colorScheme.onPrimary
    )
) {
    Text(
        text = "Refresh",
        style = MaterialTheme.typography.titleMedium
    )
}

Surface containers use background and surface colors:

Card(
    colors = CardDefaults.cardColors(
        containerColor = MaterialTheme.colorScheme.surface
    )
) {
    Text(
        text = "Repository description",
        style = MaterialTheme.typography.bodyLarge,
        color = MaterialTheme.colorScheme.onSurface
    )
}

Applying Typography with MaterialTheme.typography

Text components reference the type scale defined in Type.kt:

Text(
    text = stringResource(R.string.welcome_title),
    style = MaterialTheme.typography.titleMedium,
    color = MaterialTheme.colorScheme.onBackground
)

Summary

Frequently Asked Questions

How do I add a new custom color to the Material 3 theme?

Add a new val declaration in Color.kt with your desired hex value, then reference it in either DarkColorScheme or LightColorScheme (or both) using the appropriate Material 3 role such as primaryContainer, outline, or surfaceVariant. Any composable using MaterialTheme.colorScheme will automatically reflect the change without modification.

Can I enable dynamic colors while keeping specific brand colors?

Yes. In Theme.kt, allow dynamicColor to be true when Build.VERSION.SDK_INT >= Build.VERSION_CODES.S, which generates a scheme from the user's wallpaper. Then selectively override specific roles (such as primary) with your brand constants before passing the final colorScheme to MaterialTheme, ensuring critical brand elements remain consistent while surfaces adopt dynamic tints.

How do I change only the body text style without affecting headings?

In Type.kt, construct the Typography instance and provide values only for the specific slots you wish to customize, such as bodyLarge or labelSmall. Material 3 automatically uses default values for any unspecified text styles, so you only need to define deviations from the baseline design system.

Where should I place theme files in a multi-module project?

Place base color definitions and the main theme wrapper in a shared module (e.g., core/ui/src/main/java/com/shuyu/gsygithubappcompose/core/ui/theme/) so all feature modules can access them. Application-specific typography or theme variants can reside in the app module (app/src/main/java/com/shuyu/gsygithubappcompose/ui/theme/), as demonstrated in the gsygithubappcompose repository structure.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →