Your app works. But it looks like every other default Compose app — same purple, same fonts, same everything.

Theming is how you make your app look like YOUR app. Custom colors, custom fonts, dark mode support — all in one system that applies everywhere automatically.

Material 3 (also called Material You) is the design system Google built for modern Android apps. Compose uses it by default. In this tutorial, you will learn how to customize it.

What is Material 3?

Material 3 is a set of design rules that defines how your app looks:

  • Colors — which colors to use for buttons, backgrounds, text, etc.
  • Typography — which fonts and sizes to use
  • Shapes — how rounded corners should be

When you write MaterialTheme.colorScheme.primary, you are asking the theme: “What is the primary color right now?” The theme answers differently in light mode vs dark mode. Your UI code stays the same.

How Material 3 Theming Works in Jetpack Compose

Every Compose app already has a theme. When you created your project, Android Studio generated a Theme.kt file in the ui/theme/ folder.

The structure looks like this:

ui/theme/
├── Color.kt      — your color definitions
├── Theme.kt      — the theme that combines everything
└── Type.kt       — your typography (font) definitions

The theme wraps your entire app:

// In MainActivity.kt
setContent {
    MyAppTheme {  // Theme wraps everything
        // All Composables inside use this theme
        MyScreen()
    }
}

Every Composable inside the theme can access colors, fonts, and shapes through MaterialTheme.

Custom Colors

Defining Your Color Palette

Open Color.kt and define your colors:

// Color.kt
import androidx.compose.ui.graphics.Color

// Light theme colors
val Blue40 = Color(0xFF1565C0)
val BlueGrey40 = Color(0xFF546E7A)
val Teal40 = Color(0xFF00897B)

// Dark theme colors
val Blue80 = Color(0xFF90CAF9)
val BlueGrey80 = Color(0xFFB0BEC5)
val Teal80 = Color(0xFF80CBC4)

// Additional colors
val SurfaceLight = Color(0xFFF8F9FA)
val SurfaceDark = Color(0xFF1A1A2E)
val ErrorRed = Color(0xFFCF6679)

Building Color Schemes

In Theme.kt, create light and dark color schemes:

private val LightColorScheme = lightColorScheme(
    primary = Blue40,
    secondary = BlueGrey40,
    tertiary = Teal40,
    background = Color.White,
    surface = SurfaceLight,
    onPrimary = Color.White,
    onSecondary = Color.White,
    onBackground = Color(0xFF1C1B1F),
    onSurface = Color(0xFF1C1B1F),
    error = ErrorRed,
)

private val DarkColorScheme = darkColorScheme(
    primary = Blue80,
    secondary = BlueGrey80,
    tertiary = Teal80,
    background = Color(0xFF121212),
    surface = SurfaceDark,
    onPrimary = Color(0xFF003258),
    onSecondary = Color(0xFF1C3D5A),
    onBackground = Color(0xFFE6E1E5),
    onSurface = Color(0xFFE6E1E5),
    error = ErrorRed,
)

Understanding Color Roles

Material 3 has specific roles for each color:

ColorUsed For
primaryMain buttons, active states, links
onPrimaryText/icons on top of primary color
primaryContainerFilled cards, chips background
onPrimaryContainerText on primaryContainer
secondarySecondary buttons, less emphasis
tertiaryAccent color, third level of emphasis
backgroundScreen background
surfaceCard backgrounds, elevated areas
surfaceVariantAlternative surface (input fields, etc.)
errorError messages, validation failures
onSurfaceText on surface colors
outlineBorders, dividers

The “on” prefix means “text/icons that sit ON TOP of that color.” So onPrimary is the text color you use on a primary background.

Using Colors in Your Composables

Never hardcode colors. Always use the theme:

// BAD — hardcoded colors break dark mode
Text("Hello", color = Color.Black)
Box(modifier = Modifier.background(Color.White))

// GOOD — theme-aware colors
Text("Hello", color = MaterialTheme.colorScheme.onSurface)
Box(modifier = Modifier.background(MaterialTheme.colorScheme.surface))

Custom Typography

Defining Your Fonts

Open Type.kt:

// Type.kt
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp

val AppTypography = Typography(
    // Large titles (like screen headers)
    headlineLarge = TextStyle(
        fontSize = 32.sp,
        fontWeight = FontWeight.Bold,
        lineHeight = 40.sp
    ),
    headlineMedium = TextStyle(
        fontSize = 28.sp,
        fontWeight = FontWeight.Bold,
        lineHeight = 36.sp
    ),

    // Section titles
    titleLarge = TextStyle(
        fontSize = 22.sp,
        fontWeight = FontWeight.SemiBold,
        lineHeight = 28.sp
    ),
    titleMedium = TextStyle(
        fontSize = 16.sp,
        fontWeight = FontWeight.Medium,
        lineHeight = 24.sp
    ),

    // Body text
    bodyLarge = TextStyle(
        fontSize = 16.sp,
        lineHeight = 24.sp
    ),
    bodyMedium = TextStyle(
        fontSize = 14.sp,
        lineHeight = 20.sp
    ),

    // Small text (captions, labels)
    labelLarge = TextStyle(
        fontSize = 14.sp,
        fontWeight = FontWeight.Medium,
        lineHeight = 20.sp
    ),
    labelSmall = TextStyle(
        fontSize = 11.sp,
        lineHeight = 16.sp
    )
)

Using Typography in Composables

// Use predefined styles from the theme
Text("Screen Title", style = MaterialTheme.typography.headlineMedium)
Text("Section Header", style = MaterialTheme.typography.titleLarge)
Text("Body text goes here", style = MaterialTheme.typography.bodyLarge)
Text("Small caption", style = MaterialTheme.typography.labelSmall)

This keeps your text consistent everywhere. If you change the font size in Type.kt, it updates across the entire app.

Building the Theme

Theme.kt

@Composable
fun MyAppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    // Pick the right color scheme based on dark mode
    val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme

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

isSystemInDarkTheme() checks the system setting. When the user enables dark mode on their phone, your app switches automatically.

Dynamic Colors — Material You (Android 12+)

Android 12 introduced dynamic colors (also called Material You) — the app takes colors from the user’s wallpaper:

Note: The dynamic colors code requires these imports:

import android.os.Build
import androidx.compose.ui.platform.LocalContext
@Composable
fun MyAppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        // Use wallpaper colors on Android 12+
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context)
            else dynamicLightColorScheme(context)
        }
        // Fall back to our custom colors
        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }

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

With dynamic colors, every user’s app looks slightly different based on their wallpaper. It is a nice touch but optional.

Jetpack Compose Dark Mode Toggle

Let’s build a settings screen where the user can switch between light, dark, and system themes:

enum class ThemeMode { System, Light, Dark }

@Composable
fun ThemeSettingsScreen(
    currentMode: ThemeMode,
    onModeChange: (ThemeMode) -> Unit
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(24.dp)
    ) {
        Text("Appearance", style = MaterialTheme.typography.headlineMedium)
        Spacer(modifier = Modifier.height(24.dp))

        ThemeOption(
            title = "System Default",
            subtitle = "Follow your phone's dark mode setting",
            isSelected = currentMode == ThemeMode.System,
            onClick = { onModeChange(ThemeMode.System) }
        )

        ThemeOption(
            title = "Light",
            subtitle = "Always use light theme",
            isSelected = currentMode == ThemeMode.Light,
            onClick = { onModeChange(ThemeMode.Light) }
        )

        ThemeOption(
            title = "Dark",
            subtitle = "Always use dark theme",
            isSelected = currentMode == ThemeMode.Dark,
            onClick = { onModeChange(ThemeMode.Dark) }
        )
    }
}

@Composable
fun ThemeOption(
    title: String,
    subtitle: String,
    isSelected: Boolean,
    onClick: () -> Unit
) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clickable { onClick() }
            .padding(vertical = 12.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        RadioButton(selected = isSelected, onClick = onClick)
        Spacer(modifier = Modifier.width(12.dp))
        Column {
            Text(title, style = MaterialTheme.typography.titleMedium)
            Text(
                subtitle,
                style = MaterialTheme.typography.bodyMedium,
                color = MaterialTheme.colorScheme.onSurfaceVariant
            )
        }
    }
}

Practical Example: Themed App

Let’s build a screen that shows off the theme — cards, buttons, text styles, and colors all working together:

@Composable
fun ThemedScreen() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(MaterialTheme.colorScheme.background)
            .verticalScroll(rememberScrollState())
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        // Header using typography
        Text("My App", style = MaterialTheme.typography.headlineLarge)
        Text(
            "Built with Material 3",
            style = MaterialTheme.typography.bodyLarge,
            color = MaterialTheme.colorScheme.onSurfaceVariant
        )

        // Card using surface colors
        Surface(
            modifier = Modifier.fillMaxWidth(),
            shape = RoundedCornerShape(16.dp),
            color = MaterialTheme.colorScheme.surfaceVariant
        ) {
            Column(modifier = Modifier.padding(16.dp)) {
                Text("Featured", style = MaterialTheme.typography.titleMedium)
                Spacer(modifier = Modifier.height(4.dp))
                Text(
                    "This card uses surfaceVariant for background",
                    style = MaterialTheme.typography.bodyMedium
                )
            }
        }

        // Primary container card
        Surface(
            modifier = Modifier.fillMaxWidth(),
            shape = RoundedCornerShape(16.dp),
            color = MaterialTheme.colorScheme.primaryContainer
        ) {
            Column(modifier = Modifier.padding(16.dp)) {
                Text(
                    "Primary Container",
                    style = MaterialTheme.typography.titleMedium,
                    color = MaterialTheme.colorScheme.onPrimaryContainer
                )
                Spacer(modifier = Modifier.height(4.dp))
                Text(
                    "Great for highlighted content",
                    style = MaterialTheme.typography.bodyMedium,
                    color = MaterialTheme.colorScheme.onPrimaryContainer
                )
            }
        }

        // Buttons row
        Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            Button(onClick = {}) { Text("Primary") }
            OutlinedButton(onClick = {}) { Text("Outlined") }
            FilledTonalButton(onClick = {}) { Text("Tonal") }
        }

        // Divider
        HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)

        // Typography showcase
        Text("Typography", style = MaterialTheme.typography.titleLarge)
        Text("Headline Large", style = MaterialTheme.typography.headlineLarge)
        Text("Title Medium", style = MaterialTheme.typography.titleMedium)
        Text("Body Large", style = MaterialTheme.typography.bodyLarge)
        Text("Label Small", style = MaterialTheme.typography.labelSmall)
    }
}

This screen looks completely different in light mode vs dark mode — but you wrote ZERO code for dark mode. The theme handles everything.

Common Mistakes

Mistake 1: Hardcoding Colors

// BAD — white text is invisible on white background in light mode
Text("Hello", color = Color.White)

// GOOD — adapts to theme
Text("Hello", color = MaterialTheme.colorScheme.onSurface)

Mistake 2: Using Wrong “on” Color

// BAD — onSurface text on primary background is hard to read
Box(modifier = Modifier.background(MaterialTheme.colorScheme.primary)) {
    Text("Hello", color = MaterialTheme.colorScheme.onSurface) // Wrong!
}

// GOOD — onPrimary is designed for primary backgrounds
Box(modifier = Modifier.background(MaterialTheme.colorScheme.primary)) {
    Text("Hello", color = MaterialTheme.colorScheme.onPrimary) // Correct!
}

Mistake 3: Not Testing Dark Mode

Always test your app in both light and dark mode. Use Preview:

@Preview(name = "Light")
@Composable
fun LightPreview() {
    MyAppTheme(darkTheme = false) { ThemedScreen() }
}

@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun DarkPreview() {
    MyAppTheme(darkTheme = true) { ThemedScreen() }
}

Mistake 4: Ignoring Typography Styles

// BAD — inconsistent font sizes everywhere
Text("Title", fontSize = 24.sp, fontWeight = FontWeight.Bold)
Text("Another title", fontSize = 22.sp, fontWeight = FontWeight.SemiBold)

// GOOD — consistent through theme
Text("Title", style = MaterialTheme.typography.headlineMedium)
Text("Another title", style = MaterialTheme.typography.headlineMedium)

Quick Reference

Color Scheme

RoleLight UsageDark Usage
primaryMain accent colorLighter version
primaryContainerTinted backgroundsDarker tinted backgrounds
surfaceCard/sheet backgroundsDark card backgrounds
backgroundScreen backgroundDark screen background
errorError indicatorsError indicators

Typography Scale

StyleUse For
headlineLargeScreen titles
headlineMediumSection headers
titleLargeCard titles
titleMediumSubsection titles
bodyLargeMain body text
bodyMediumSecondary body text
labelLargeButton text
labelSmallCaptions, timestamps

Result

Here is what the themed app looks like:

Light ModeDark Mode
Tutorial 7 LightTutorial 7 Dark

Source Code

The complete working code for this tutorial is on GitHub:

View source code on GitHub →

What’s Next?

Part 1 (Foundations) is complete! In the next tutorial, we start Part 2 (Intermediate) with Navigation — moving between screens using NavHost, NavController, and bottom navigation bars.

See you there.