If you’ve been diving into modern Android development, you’ve probably heard the buzz about Material Design 3 (also known as Material You) and Jetpack Compose. Today, we’re going to explore one of the most powerful yet underappreciated features that ties them together: Design Tokens.
Understanding Design Tokens in Material 3 and Jetpack Compose will transform how you build consistent, beautiful, and maintainable Android apps.
Let’s dive in..!
What Are Design Tokens?
Before we jump into the Material 3 specifics, let’s get on the same page about what design tokens actually are.
Think of design tokens as the DNA of your app’s design system. They’re named values that store design decisions like colors, typography, spacing, and shapes. Instead of hardcoding Color(0xFF6200EE) everywhere in your app, you’d use a token like MaterialTheme.colorScheme.primary.
Btw why this matters..?
Actually, when you decide to rebrand your app or support dark mode, you only need to change the token values in one place, not hunt down hundreds of hardcoded values scattered across your codebase.
Why Material Design 3 Changed Everything
Material Design 3 represents a massive evolution in how we think about design systems. Unlike Material Design 2, which had a more rigid structure, Material 3 introduces a flexible, personalized approach that adapts to user preferences.
Design Tokens in Material 3 and Jetpack Compose work together to make this personalization possible. Material 3 includes over 40 color tokens, dynamic color generation from wallpapers, and a comprehensive token system for typography, shapes, and elevation.
Understanding the Material Design 3 Token Structure
Material Design 3 organizes tokens into structured layers:
1. Reference Tokens
Raw values like colors or sizes.
Example:
- Blue 500
- 16sp
- 8dp
2. System Tokens
Semantic values used by the UI system.
Example:
primaryonPrimarysurface
3. Component Tokens
Values applied to specific UI components.
Example:
- Button container color
- TextField label color
Jetpack Compose primarily exposes system tokens through MaterialTheme, which internally map to component behavior.
Material Design 3 in Jetpack Compose
Jetpack Compose provides the MaterialTheme composable (from the material3 library) that exposes design tokens:
colorSchemetypographyshapes
Let’s explore each with Kotlin examples.
The Core Components of Design Tokens in Material 3
Let’s break down the main categories of design tokens you’ll work with:
1. Color Tokens
Material 3’s color system is brilliant. Instead of just “primary” and “secondary,” you get a full palette that automatically handles light and dark modes, accessibility, and color harmonies.
@Composable
fun ColorTokenExample() {
// Access color tokens through MaterialTheme
val primaryColor = MaterialTheme.colorScheme.primary
val onPrimary = MaterialTheme.colorScheme.onPrimary
val surface = MaterialTheme.colorScheme.surface
val surfaceVariant = MaterialTheme.colorScheme.surfaceVariant
Card(
colors = CardDefaults.cardColors(
containerColor = surface,
contentColor = MaterialTheme.colorScheme.onSurface
)
) {
Text(
text = "This uses design tokens!",
color = MaterialTheme.colorScheme.onSurface
)
}
}Here, we’re accessing color tokens through MaterialTheme.colorScheme. The MaterialTheme composable provides access to Material 3’s design tokens. These tokens automatically adjust based on whether the user is in light or dark mode. The onPrimary token ensures text on your primary color is always readable.
2. Typography Tokens
Typography tokens define your text styles consistently across your app. Material Design 3 provides a complete type scale with tokens for everything from large display text to tiny labels.
@Composable
fun TypographyTokenExample() {
Column(
modifier = Modifier.padding(16.dp)
) {
// Display large - for prominent text
Text(
text = "Welcome Back!",
style = MaterialTheme.typography.displayLarge
)
// Headline medium - for section headers
Text(
text = "Your Dashboard",
style = MaterialTheme.typography.headlineMedium
)
// Body large - for main content
Text(
text = "Here's a summary of your activity today.",
style = MaterialTheme.typography.bodyLarge
)
// Label small - for captions or metadata
Text(
text = "Last updated: 2 hours ago",
style = MaterialTheme.typography.labelSmall
)
}
}Each typography token (displayLarge, headlineMedium, bodyLarge, labelSmall) defines font size, weight, line height, and letter spacing. By using these Material 3 tokens instead of hardcoding text styles, your app maintains perfect typographic hierarchy.
3. Shape Tokens
Shapes define the corner radii and other geometric properties of your components. Material Design 3 uses different shape tokens for different component types.
@Composable
fun ShapeTokenExample() {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
// Extra small - for chips and small elements
Surface(
shape = MaterialTheme.shapes.extraSmall,
color = MaterialTheme.colorScheme.primaryContainer,
modifier = Modifier.size(60.dp)
) {
Box(contentAlignment = Alignment.Center) {
Text("XS")
}
}
// Medium - for cards
Surface(
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.secondaryContainer,
modifier = Modifier.size(60.dp)
) {
Box(contentAlignment = Alignment.Center) {
Text("M")
}
}
// Large - for dialogs and sheets
Surface(
shape = MaterialTheme.shapes.large,
color = MaterialTheme.colorScheme.tertiaryContainer,
modifier = Modifier.size(60.dp)
) {
Box(contentAlignment = Alignment.Center) {
Text("L")
}
}
}
}Shape tokens (extraSmall, medium, large) ensure consistent corner radii throughout your app. Material 3 uses different shapes for different component types, creating visual cohesion and helping users understand component hierarchy.
Setting Up Design Tokens in Your Jetpack Compose Project
Now let’s get practical. Here’s how to implement Design Tokens in Material 3 and Jetpack Compose in your project.
Add Material 3 Dependency
First, ensure you have the Material 3 library in your build.gradle.kts file:
dependencies {
implementation("androidx.compose.material3:material3:1.2.0")
implementation("androidx.compose.ui:ui:1.6.0")
}Create Your Color Scheme
Material Design 3 makes it easy to generate a complete color scheme. You can use the Material Theme Builder tool or define colors manually.
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.ui.graphics.Color
// Define your seed colors
private val md_theme_light_primary = Color(0xFF6750A4)
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
private val md_theme_light_primaryContainer = Color(0xFFEADDFF)
private val md_theme_light_onPrimaryContainer = Color(0xFF21005D)
private val md_theme_light_secondary = Color(0xFF625B71)
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
private val md_theme_light_secondaryContainer = Color(0xFFE8DEF8)
private val md_theme_light_onSecondaryContainer = Color(0xFF1D192B)
private val md_theme_light_surface = Color(0xFFFFFBFE)
private val md_theme_light_onSurface = Color(0xFF1C1B1F)
private val md_theme_dark_primary = Color(0xFFD0BCFF)
private val md_theme_dark_onPrimary = Color(0xFF381E72)
private val md_theme_dark_primaryContainer = Color(0xFF4F378B)
private val md_theme_dark_onPrimaryContainer = Color(0xFFEADDFF)
private val md_theme_dark_secondary = Color(0xFFCCC2DC)
private val md_theme_dark_onSecondary = Color(0xFF332D41)
private val md_theme_dark_secondaryContainer = Color(0xFF4A4458)
private val md_theme_dark_onSecondaryContainer = Color(0xFFE8DEF8)
private val md_theme_dark_surface = Color(0xFF1C1B1F)
private val md_theme_dark_onSurface = Color(0xFFE6E1E5)
val LightColorScheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface
)
val DarkColorScheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface
)We’re defining two color schemes — one for light mode and one for dark mode. This follows the Material Design 3 color system specification. Each color has a specific purpose.
Notice the “on” prefix..? Those ensure text and icons are readable on their corresponding background colors.
Create Your Custom Theme
Now let’s wrap everything in a theme composable. This is where we configure the MaterialTheme composable with our Material 3 design tokens:
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Typography
import androidx.compose.material3.Shapes
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Custom typography following Material Design 3 type scale
val AppTypography = Typography(
displayLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Bold,
fontSize = 57.sp,
lineHeight = 64.sp,
letterSpacing = (-0.25).sp
),
headlineMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.SemiBold,
fontSize = 28.sp,
lineHeight = 36.sp,
letterSpacing = 0.sp
),
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
)
@Composable
fun MyAppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (darkTheme) {
DarkColorScheme
} else {
LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
shapes = Shapes(), // Uses default Material 3 shapes
content = content
)
}The MyAppTheme composable automatically detects if the system is in dark mode and switches between your light and dark color schemes. We pass our design tokens to the MaterialTheme composable, which makes them available throughout your app. We’re defining custom typography based on Material Design 3’s type scale while using Material 3’s default shapes.
Apply Your Theme
Wrap your app’s root composable with your theme:
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
// Surface provides a background using the surface color token
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
// Your app content goes here
AppContent()
}
}
}
}
}By wrapping everything in MyAppTheme, all composables inside can access your Material 3 design tokens through MaterialTheme. The Surface composable uses the background color token automatically.
Advanced: Dynamic Color and Material You
One of the coolest features of Design Tokens in Material 3 and Jetpack Compose is dynamic color. On Android 12+, your app can generate its color scheme from the user’s wallpaper..!
This is the signature feature of Material You (Material Design 3’s brand name), creating truly personalized user experiences.
import android.os.Build
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
@Composable
fun MyAppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true, // Enable dynamic color
content: @Composable () -> Unit
) {
val colorScheme = when {
// Use dynamic 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 custom colors
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
content = content
)
}On devices running Android 12 or higher, dynamicLightColorScheme() and dynamicDarkColorScheme() generate a complete Material 3 color scheme based on the user’s wallpaper. This creates a truly personalized experience without any extra work on your part! Your design tokens automatically adapt to the generated colors.
Creating Custom Design Tokens
Sometimes you need tokens beyond what Material 3 provides. Here’s how to extend the system while maintaining consistency with Material Design 3 principles:
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
// Define custom spacing tokens
data class Spacing(
val none: Dp = 0.dp,
val extraSmall: Dp = 4.dp,
val small: Dp = 8.dp,
val medium: Dp = 16.dp,
val large: Dp = 24.dp,
val extraLarge: Dp = 32.dp,
val huge: Dp = 48.dp
)
// Create a CompositionLocal
val LocalSpacing = staticCompositionLocalOf { Spacing() }
// Extension property for easy access
val MaterialTheme.spacing: Spacing
@Composable
get() = LocalSpacing.current
// Usage in your theme
@Composable
fun MyAppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography
) {
// Provide custom spacing tokens
CompositionLocalProvider(LocalSpacing provides Spacing()) {
content()
}
}
}
// Now you can use custom spacing tokens!
@Composable
fun CustomSpacingExample() {
Column(
modifier = Modifier.padding(MaterialTheme.spacing.medium)
) {
Text(
text = "First item",
modifier = Modifier.padding(bottom = MaterialTheme.spacing.small)
)
Text(
text = "Second item",
modifier = Modifier.padding(bottom = MaterialTheme.spacing.large)
)
}
}We created custom spacing tokens using CompositionLocal, which allows us to provide values that can be accessed by any composable in the tree. The extension property makes accessing these tokens feel natural, just like accessing built-in Material 3 tokens. This approach maintains consistency with how Material Design 3 organizes its design system.
Best Practices for Design Tokens
Working with Design Tokens in Material 3 and Jetpack Compose effectively requires following some key principles:
Always Use Tokens, Never Hardcode
Bad:
Text(
text = "Hello",
color = Color(0xFF6750A4), // Hardcoded color
fontSize = 16.sp // Hardcoded size
)Good:
Text(
text = "Hello",
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.bodyLarge
)Use Semantic Token Names
When creating custom tokens, use names that describe the purpose, not the appearance. This follows Material Design 3’s semantic naming philosophy:
Bad: val blueButton = Color(0xFF0000FF)
Good: val buttonPrimary = MaterialTheme.colorScheme.primary
Leverage “On” Color Tokens
Material 3 provides “on” tokens that ensure proper contrast:
@Composable
fun AccessibleButton() {
Button(
onClick = { },
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary // Always readable!
)
) {
Text("Click Me")
}
}The onPrimary token adjusts automatically to maintain proper contrast ratio for accessibility, whether you’re in light mode, dark mode, or using dynamic colors. This is a core principle of Material Design 3’s accessibility-first approach.
Real-World Example: Building a Themed Card Component
Let’s put everything together with a practical example that showcases Design Tokens in Material 3 and Jetpack Compose:
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun ProductCard(
title: String,
description: String,
price: String,
onFavoriteClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier.fillMaxWidth(),
// Using Material 3 shape token
shape = MaterialTheme.shapes.medium,
// Using Material 3 color tokens
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// Using Material 3 typography token
Text(
text = title,
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
IconButton(onClick = onFavoriteClick) {
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = "Add to favorites",
tint = MaterialTheme.colorScheme.primary
)
}
}
Spacer(modifier = Modifier.height(8.dp))
// Using Material 3 typography token for body text
Text(
text = description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f)
)
Spacer(modifier = Modifier.height(12.dp))
// Using Material 3 typography token for price
Text(
text = price,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary
)
}
}
}
// Using the component
@Composable
fun ProductScreen() {
MyAppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
ProductCard(
title = "Wireless Headphones",
description = "Premium noise-cancelling headphones with 30-hour battery life.",
price = "$299.99",
onFavoriteClick = { /* Handle favorite */ }
)
}
}
}
}This ProductCard component uses Material 3 design tokens exclusively. It automatically adapts to light/dark mode, respects dynamic colors from Material You, maintains proper typography hierarchy, and ensures all text is readable against its background. That’s the power of Material Design 3’s token-based system!
Testing Your Design Tokens
Want to make sure your Material 3 tokens work in all scenarios? Create a preview showcase:
import androidx.compose.ui.tooling.preview.Preview
@Preview(name = "Light Mode", showBackground = true)
@Composable
fun ProductCardLightPreview() {
MyAppTheme(darkTheme = false) {
ProductCard(
title = "Wireless Headphones",
description = "Premium noise-cancelling headphones with 30-hour battery life.",
price = "$299.99",
onFavoriteClick = { }
)
}
}
@Preview(name = "Dark Mode", showBackground = true)
@Composable
fun ProductCardDarkPreview() {
MyAppTheme(darkTheme = true) {
ProductCard(
title = "Wireless Headphones",
description = "Premium noise-cancelling headphones with 30-hour battery life.",
price = "$299.99",
onFavoriteClick = { }
)
}
}Pro tip: Android Studio shows these previews side-by-side, letting you verify that your Material 3 design tokens create a cohesive experience in both light and dark modes.
Common Mistakes to Avoid
Mistake 1: Mixing Hardcoded and Token Values
Don’t do this:
Text(
text = "Title",
fontSize = 24.sp, // Hardcoded
color = MaterialTheme.colorScheme.primary // Token
)Instead:
Text(
text = "Title",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.primary
)Mistake 2: Forgetting About Accessibility
Always use “on” color tokens for text on colored backgrounds. Material Design 3 emphasizes accessibility:
// This might have poor contrast
Button(
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.tertiary,
contentColor = Color.Gray // Bad!
)
) { Text("Submit") }
// This ensures proper contrast following Material 3 guidelines
Button(
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.tertiary,
contentColor = MaterialTheme.colorScheme.onTertiary // Good!
)
) { Text("Submit") }Mistake 3: Not Testing in Both Modes
Always preview your composables in both light and dark modes to ensure your Material 3 token usage works correctly.
Why Design Tokens Improve Long-Term Maintainability
Design tokens:
- Reduce UI bugs
- Speed up redesigns
- Improve accessibility
- Keep your codebase cleaner
- Align perfectly with Material Design 3 principles
This is why using design tokens with Material Design 3 in Jetpack Compose is strongly recommended.
Conclusion
Understanding and implementing Design Tokens in Material 3 and Jetpack Compose transforms your development workflow. You get:
- Consistency: Every component uses the same Material Design 3 language
- Maintainability: Change your entire theme by updating token values
- Accessibility: Automatic contrast ratios and readability
- Personalization: Dynamic colors that adapt to user preferences through Material You
- Scalability: Easy to extend with custom tokens while maintaining Material 3 principles
The examples we’ve covered today give you a solid foundation to build beautiful, consistent Android apps following Material Design 3 guidelines. Start by implementing basic color and typography tokens, then gradually expand to custom tokens as your needs grow.
Remember, the key to mastering Design Tokens in Material 3 and Jetpack Compose is practice. Start refactoring your existing projects to use Material 3 tokens, and you’ll quickly see the benefits of this systematic approach.
