How to Build a Consistent Custom App Theme in Jetpack Compose (Material 3)
Creating a polished Android app starts with one crucial decision: your app’s visual identity. If you’ve been wondering how to make your Jetpack Compose app look consistent and professional across every screen, you’re in the right place.
In this guide, I’ll walk you through building a Custom App Theme in Jetpack Compose using Material 3. Whether you’re building your first app or refining an existing one, you’ll learn how to create a theming system that’s both flexible and maintainable.
Why Material 3 Makes Custom Theming Easier
Material 3 (also called Material You) isn’t just another design system update. It’s Google’s most flexible theming framework yet, and it plays beautifully with Jetpack Compose.
Here’s what makes it special:
Dynamic color support — Your app can adapt to the user’s wallpaper colors (on Android 12+)
Improved design tokens — More granular control over colors, typography, and shapes Better accessibility — Built-in contrast and readability improvements
The best part..?
Once you set up your Custom App Theme in Jetpack Compose, Material 3 handles the heavy lifting of maintaining consistency throughout your app.
Understanding the Theme Building Blocks
Before we dive into code, let’s understand what makes up a theme in Jetpack Compose. Think of it like building a house — you need a solid foundation.
Your theme consists of three main pillars:
- Color Scheme — All the colors your app uses
- Typography — Font families, sizes, and weights
- Shapes — Corner radiuses and component shapes
When these three work together harmoniously, your app feels intentional and polished.
Setting Up Your Project Dependencies
First things first — make sure you have the right dependencies in your build.gradle.kts file:
dependencies {
implementation("androidx.compose.material3:material3:1.2.0")
implementation("androidx.compose.ui:ui:1.6.0")
implementation("androidx.compose.ui:ui-tooling-preview:1.6.0")
}These dependencies give you access to Material 3 components and theming capabilities.
Always check for the latest stable versions on the official documentation.
Creating Your Custom Color Scheme
Colors are the personality of your app. Let’s create a custom color palette that reflects your brand.
Define Your Color Palette
Create a new Kotlin file called Color.kt in your UI theme package:
package com.yourapp.ui.theme
import androidx.compose.ui.graphics.Color
// Light Theme Colors
val PrimaryLight = Color(0xFF6750A4)
val OnPrimaryLight = Color(0xFFFFFFFF)
val PrimaryContainerLight = Color(0xFFEADDFF)
val OnPrimaryContainerLight = Color(0xFF21005D)
val SecondaryLight = Color(0xFF625B71)
val OnSecondaryLight = Color(0xFFFFFFFF)
val SecondaryContainerLight = Color(0xFFE8DEF8)
val OnSecondaryContainerLight = Color(0xFF1D192B)
val TertiaryLight = Color(0xFF7D5260)
val OnTertiaryLight = Color(0xFFFFFFFF)
val TertiaryContainerLight = Color(0xFFFFD8E4)
val OnTertiaryContainerLight = Color(0xFF31111D)
val ErrorLight = Color(0xFFB3261E)
val OnErrorLight = Color(0xFFFFFFFF)
val ErrorContainerLight = Color(0xFFF9DEDC)
val OnErrorContainerLight = Color(0xFF410E0B)
val BackgroundLight = Color(0xFFFFFBFE)
val OnBackgroundLight = Color(0xFF1C1B1F)
val SurfaceLight = Color(0xFFFFFBFE)
val OnSurfaceLight = Color(0xFF1C1B1F)
// Dark Theme Colors
val PrimaryDark = Color(0xFFD0BCFF)
val OnPrimaryDark = Color(0xFF381E72)
val PrimaryContainerDark = Color(0xFF4F378B)
val OnPrimaryContainerDark = Color(0xFFEADDFF)
val SecondaryDark = Color(0xFFCCC2DC)
val OnSecondaryDark = Color(0xFF332D41)
val SecondaryContainerDark = Color(0xFF4A4458)
val OnSecondaryContainerDark = Color(0xFFE8DEF8)
val TertiaryDark = Color(0xFFEFB8C8)
val OnTertiaryDark = Color(0xFF492532)
val TertiaryContainerDark = Color(0xFF633B48)
val OnTertiaryContainerDark = Color(0xFFFFD8E4)
val ErrorDark = Color(0xFFF2B8B5)
val OnErrorDark = Color(0xFF601410)
val ErrorContainerDark = Color(0xFF8C1D18)
val OnErrorContainerDark = Color(0xFFF9DEDC)
val BackgroundDark = Color(0xFF1C1B1F)
val OnBackgroundDark = Color(0xFFE6E1E5)
val SurfaceDark = Color(0xFF1C1B1F)
val OnSurfaceDark = Color(0xFFE6E1E5)Here,
Each color has a specific role in Material 3. The naming convention follows a pattern:
Primary– Your brand’s main colorOnPrimary– Text/icon color that appears on top of primaryPrimaryContainer– A lighter shade for containersOnPrimaryContainer– Text/icons on primary containers
This naming system ensures your Custom App Theme in Jetpack Compose maintains proper contrast and readability automatically.
Build Your Color Schemes
Now let’s create the actual color scheme objects. Add this to a new file called Theme.kt:
package com.yourapp.ui.theme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
private val LightColorScheme = lightColorScheme(
primary = PrimaryLight,
onPrimary = OnPrimaryLight,
primaryContainer = PrimaryContainerLight,
onPrimaryContainer = OnPrimaryContainerLight,
secondary = SecondaryLight,
onSecondary = OnSecondaryLight,
secondaryContainer = SecondaryContainerLight,
onSecondaryContainer = OnSecondaryContainerLight,
tertiary = TertiaryLight,
onTertiary = OnTertiaryLight,
tertiaryContainer = TertiaryContainerLight,
onTertiaryContainer = OnTertiaryContainerLight,
error = ErrorLight,
onError = OnErrorLight,
errorContainer = ErrorContainerLight,
onErrorContainer = OnErrorContainerLight,
background = BackgroundLight,
onBackground = OnBackgroundLight,
surface = SurfaceLight,
onSurface = OnSurfaceLight,
)
private val DarkColorScheme = darkColorScheme(
primary = PrimaryDark,
onPrimary = OnPrimaryDark,
primaryContainer = PrimaryContainerDark,
onPrimaryContainer = OnPrimaryContainerDark,
secondary = SecondaryDark,
onSecondary = OnSecondaryDark,
secondaryContainer = SecondaryContainerDark,
onSecondaryContainer = OnSecondaryContainerDark,
tertiary = TertiaryDark,
onTertiary = OnTertiaryDark,
tertiaryContainer = TertiaryContainerDark,
onTertiaryContainer = OnTertiaryContainerDark,
error = ErrorDark,
onError = OnErrorDark,
errorContainer = ErrorContainerDark,
onErrorContainer = OnErrorContainerDark,
background = BackgroundDark,
onBackground = OnBackgroundDark,
surface = SurfaceDark,
onSurface = OnSurfaceDark,
)These functions (lightColorScheme and darkColorScheme) are provided by Material 3 to ensure your colors meet accessibility standards.
Customizing Typography
Typography shapes how users read and understand your content. Let’s create a type scale that’s both beautiful and functional.
Defining Custom Fonts
Create a Type.kt file:
package com.yourapp.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Define your custom font families
// Make sure to add your font files to res/font/
val Montserrat = FontFamily(
Font(R.font.montserrat_regular, FontWeight.Normal),
Font(R.font.montserrat_medium, FontWeight.Medium),
Font(R.font.montserrat_semibold, FontWeight.SemiBold),
Font(R.font.montserrat_bold, FontWeight.Bold)
)
val Roboto = FontFamily(
Font(R.font.roboto_regular, FontWeight.Normal),
Font(R.font.roboto_medium, FontWeight.Medium),
Font(R.font.roboto_bold, FontWeight.Bold)
)
// Custom Typography
val AppTypography = Typography(
// Display styles - for large, impactful text
displayLarge = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.Bold,
fontSize = 57.sp,
lineHeight = 64.sp,
letterSpacing = (-0.25).sp
),
displayMedium = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.Bold,
fontSize = 45.sp,
lineHeight = 52.sp,
letterSpacing = 0.sp
),
displaySmall = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.Bold,
fontSize = 36.sp,
lineHeight = 44.sp,
letterSpacing = 0.sp
),
// Headline styles - for section headers
headlineLarge = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.SemiBold,
fontSize = 32.sp,
lineHeight = 40.sp,
letterSpacing = 0.sp
),
headlineMedium = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.SemiBold,
fontSize = 28.sp,
lineHeight = 36.sp,
letterSpacing = 0.sp
),
headlineSmall = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.SemiBold,
fontSize = 24.sp,
lineHeight = 32.sp,
letterSpacing = 0.sp
),
// Title styles - for card titles and important text
titleLarge = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.Medium,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
titleMedium = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.15.sp
),
titleSmall = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
),
// Body styles - for main content
bodyLarge = TextStyle(
fontFamily = Roboto,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
bodyMedium = TextStyle(
fontFamily = Roboto,
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.25.sp
),
bodySmall = TextStyle(
fontFamily = Roboto,
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.4.sp
),
// Label styles - for buttons and small UI elements
labelLarge = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
),
labelMedium = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.Medium,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
),
labelSmall = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
)Understanding the type scale:
Material 3 provides 15 different text styles organized into five categories. This gives you flexibility while maintaining consistency in your Custom App Theme in Jetpack Compose:
- Display — Hero text, splash screens
- Headline — Page titles, section headers
- Title — Card titles, dialog headers
- Body — Paragraphs, main content
- Label — Buttons, tabs, small UI elements
Defining Custom Shapes
Shapes add personality to your UI. From rounded corners to sharp edges, shapes influence how modern or traditional your app feels.
Create a Shape.kt file:
package com.yourapp.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Shapes
import androidx.compose.ui.unit.dp
val AppShapes = Shapes(
// Extra small - chips, small buttons
extraSmall = RoundedCornerShape(4.dp),
// Small - buttons, text fields
small = RoundedCornerShape(8.dp),
// Medium - cards, dialogs
medium = RoundedCornerShape(12.dp),
// Large - bottom sheets, large cards
large = RoundedCornerShape(16.dp),
// Extra large - special components
extraLarge = RoundedCornerShape(28.dp)
)Shape usage tips:
- Use
extraSmallfor chips and toggles - Use
smallfor buttons and input fields - Use
mediumfor cards and elevated surfaces - Use
largefor bottom sheets and modals - Use
extraLargefor floating action buttons
You can also create asymmetric shapes or custom shapes using GenericShape for more creative designs.
Bringing It All Together: Your Theme Composable
Now comes the exciting part — assembling everything into your main theme composable. Update your Theme.kt file:
package com.yourapp.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true, // Dynamic color available on Android 12+
content: @Composable () -> Unit
) {
val colorScheme = when {
// Dynamic color (Material You) - uses user's wallpaper colors
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
}
// Dark theme
darkTheme -> DarkColorScheme
// Light theme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
shapes = AppShapes,
content = content
)
}- Dark theme detection — Automatically detects if the user prefers dark mode
- Dynamic color support — On Android 12+, colors adapt to the user’s wallpaper
- Status bar styling — Ensures the status bar matches your theme
- Fallback colors — Uses your custom colors on older Android versions
This is the heart of your Custom App Theme in Jetpack Compose. Every screen that uses this theme will automatically have consistent colors, typography, and shapes.
Using Your Theme in the App
Now let’s see how to apply your theme to your app. In your MainActivity.kt:
package com.yourapp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.yourapp.ui.theme.AppTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
HomeScreen()
}
}
}
}
}
@Composable
fun HomeScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Using themed text styles
Text(
text = "Welcome to My App",
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.primary
)
Text(
text = "This is a subtitle showing our custom typography",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = "Body text looks great with our custom font family. " +
"Notice how everything feels cohesive and professional.",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
// Using themed button with custom shapes
Button(
onClick = { /* Handle click */ },
shape = MaterialTheme.shapes.medium
) {
Text("Primary Button")
}
// Using themed card
Card(
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Card Title",
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "This card uses our theme colors and shapes automatically.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
}
}
@Preview(showBackground = true)
@Composable
fun HomeScreenPreview() {
AppTheme {
HomeScreen()
}
}Key takeaways from this example:
- Wrap your content in
AppTheme { }to apply your custom theme - Access colors via
MaterialTheme.colorScheme.primary(not hardcoded values!) - Access typography via
MaterialTheme.typography.headlineLarge - Access shapes via
MaterialTheme.shapes.medium
This approach ensures your Custom App Theme in Jetpack Compose is applied consistently throughout your app.
Creating Theme-Aware Components
Let’s build a custom component that respects your theme. This is where the real power of theming shines:
@Composable
fun ThemedInfoCard(
title: String,
description: String,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
Divider(
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
thickness = 1.dp
)
Text(
text = description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}
}
// Usage
@Composable
fun ExampleScreen() {
Column(modifier = Modifier.padding(16.dp)) {
ThemedInfoCard(
title = "Themed Component",
description = "This card automatically adapts to light/dark theme changes!"
)
}
}This component will automatically look perfect in both light and dark modes because it uses theme values instead of hardcoded colors.
Advanced: Adding Custom Theme Values
Sometimes you need values beyond what Material 3 provides. Here’s how to extend your theme:
// Create custom theme values
data class CustomColors(
val success: Color,
val onSuccess: Color,
val warning: Color,
val onWarning: Color,
val info: Color,
val onInfo: Color
)
// Light theme custom colors
val LightCustomColors = CustomColors(
success = Color(0xFF4CAF50),
onSuccess = Color(0xFFFFFFFF),
warning = Color(0xFFFF9800),
onWarning = Color(0xFF000000),
info = Color(0xFF2196F3),
onInfo = Color(0xFFFFFFFF)
)
// Dark theme custom colors
val DarkCustomColors = CustomColors(
success = Color(0xFF81C784),
onSuccess = Color(0xFF000000),
warning = Color(0xFFFFB74D),
onWarning = Color(0xFF000000),
info = Color(0xFF64B5F6),
onInfo = Color(0xFF000000)
)
// Create a CompositionLocal
val LocalCustomColors = staticCompositionLocalOf { LightCustomColors }
// Extend your theme
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val customColors = if (darkTheme) DarkCustomColors else LightCustomColors
// ... existing color scheme code ...
CompositionLocalProvider(LocalCustomColors provides customColors) {
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
shapes = AppShapes,
content = content
)
}
}
// Access custom colors
@Composable
fun CustomThemedComponent() {
val customColors = LocalCustomColors.current
Button(
onClick = { },
colors = ButtonDefaults.buttonColors(
containerColor = customColors.success
)
) {
Text("Success Action", color = customColors.onSuccess)
}
}This technique lets you add semantic colors like success, warning, and info to your Custom App Theme in Jetpack Compose.
Best Practices for Theme Consistency
Here are proven strategies to keep your theme consistent as your app grows:
1. Never Hardcode Colors
Bad:
Text(
text = "Hello",
color = Color(0xFF6750A4) // Hardcoded!
)Good:
Text(
text = "Hello",
color = MaterialTheme.colorScheme.primary
)2. Create Reusable Components
Instead of repeating styling code, create themed components:
@Composable
fun AppButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
isPrimary: Boolean = true
) {
Button(
onClick = onClick,
modifier = modifier,
colors = ButtonDefaults.buttonColors(
containerColor = if (isPrimary)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.secondary
),
shape = MaterialTheme.shapes.medium
) {
Text(
text = text,
style = MaterialTheme.typography.labelLarge
)
}
}3. Use Semantic Naming
Choose color roles based on meaning, not appearance:
// Bad naming
val BlueColor = Color(0xFF2196F3)
// Good naming
val LinkColor = MaterialTheme.colorScheme.primary
val SuccessColor = customColors.success4. Test Both Themes
Always preview your screens in both light and dark modes:
@Preview(name = "Light Mode", showBackground = true)
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun ScreenPreview() {
AppTheme {
YourScreen()
}
}5. Keep Typography Consistent
Use the defined typography scale instead of creating custom text styles on the fly:
// Avoid this
Text(
text = "Title",
fontSize = 24.sp,
fontWeight = FontWeight.Bold
)
// Do this
Text(
text = "Title",
style = MaterialTheme.typography.headlineSmall
)Debugging Theme Issues
Sometimes things don’t look right. Here’s how to troubleshoot:
Preview Your Theme Values
Create a debug screen to visualize your theme:
@Composable
fun ThemeDebugScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
Text("Color Scheme", style = MaterialTheme.typography.headlineMedium)
Spacer(modifier = Modifier.height(8.dp))
ColorBox("Primary", MaterialTheme.colorScheme.primary)
ColorBox("Secondary", MaterialTheme.colorScheme.secondary)
ColorBox("Tertiary", MaterialTheme.colorScheme.tertiary)
// ... add more colors
Spacer(modifier = Modifier.height(16.dp))
Text("Typography", style = MaterialTheme.typography.headlineMedium)
Text("Display Large", style = MaterialTheme.typography.displayLarge)
Text("Headline Medium", style = MaterialTheme.typography.headlineMedium)
Text("Body Large", style = MaterialTheme.typography.bodyLarge)
// ... add more styles
}
}
@Composable
fun ColorBox(name: String, color: Color) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(name)
Box(
modifier = Modifier
.size(40.dp)
.background(color, MaterialTheme.shapes.small)
)
}
}This screen helps you visually verify all your theme values at a glance.
Supporting Dynamic Color (Material You)
Material You allows users to personalize their experience. Here’s how to give users control:
// In your ViewModel or state management
class ThemeViewModel : ViewModel() {
private val _useDynamicColor = MutableStateFlow(true)
val useDynamicColor: StateFlow<Boolean> = _useDynamicColor.asStateFlow()
fun toggleDynamicColor() {
_useDynamicColor.value = !_useDynamicColor.value
}
}
// In your settings screen
@Composable
fun SettingsScreen(viewModel: ThemeViewModel = viewModel()) {
val useDynamicColor by viewModel.useDynamicColor.collectAsState()
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("Use Material You colors")
Switch(
checked = useDynamicColor,
onCheckedChange = { viewModel.toggleDynamicColor() }
)
}
}Users on Android 12+ can then choose between your custom colors and colors that match their wallpaper.
Common Mistakes to Avoid
Mistake 1: Mixing Hardcoded and Theme Values
Don’t mix approaches in the same project. Pick theme values and stick with them:
// Inconsistent - Don't do this
Column {
Text(text = "Title", color = MaterialTheme.colorScheme.primary)
Text(text = "Subtitle", color = Color.Gray) // Hardcoded!
}
// Consistent - Do this
Column {
Text(text = "Title", color = MaterialTheme.colorScheme.primary)
Text(text = "Subtitle", color = MaterialTheme.colorScheme.onSurfaceVariant)
}
Mistake 2: Ignoring Accessibility
Always ensure sufficient contrast between text and backgrounds:
// Check contrast ratios
val backgroundColor = MaterialTheme.colorScheme.primary
val textColor = MaterialTheme.colorScheme.onPrimary // Guaranteed good contrastMaterial 3 handles this automatically if you use the correct color pairs (primary/onPrimary, surface/onSurface, etc.).
Mistake 3: Over-Customizing
Not every component needs custom styling. Sometimes the default Material 3 styling is perfect:
// Often unnecessary
Button(
onClick = { },
colors = ButtonDefaults.buttonColors(/* custom colors */),
shape = RoundedCornerShape(/* custom shape */),
elevation = ButtonDefaults.elevatedButtonElevation(/* custom elevation */)
) { }
// Usually sufficient
Button(onClick = { }) {
Text("Click Me")
}Migration from Material 2 to Material 3
If you’re upgrading an existing app, here’s a quick migration guide:
Color Migration
Material 2 → Material 3:
primary→primary(similar)primaryVariant→primaryContainersecondary→secondary(similar)secondaryVariant→secondaryContainerbackground→background(same)surface→surface(same)
Typography Migration
Material 3 has more granular typography styles. Map your old styles:
h1→displayLargeh2→displayMediumh3→displaySmallh4→headlineLargeh5→headlineMediumh6→headlineSmallsubtitle1→titleLargesubtitle2→titleMediumbody1→bodyLargebody2→bodyMedium
Conclusion
Building a Custom App Theme in Jetpack Compose with Material 3 might seem complex at first, but it’s an investment that pays dividends. You get:
- Consistency — Every screen automatically follows your design system
- Maintainability — Change one value, update the entire app
- Flexibility — Support light/dark themes and Material You effortlessly
- Professionalism — Your app looks polished and well-crafted
- Accessibility — Built-in contrast and readability standards
The key is starting with a solid foundation: well-defined colors, typography, and shapes. Once your theme is set up, building new screens becomes faster because you’re working with a consistent design language.
Remember, your Custom App Theme in Jetpack Compose isn’t set in stone. As your app evolves and your brand matures, you can refine your theme values. The beauty of this system is that those updates propagate throughout your entire app automatically.




















