Compose Multiplatform Theme and Resources Guide
Key Points
- Creating
composeResources - Defining typography
- Defining colors and color schemes
- Defining extended theme colors
- Defining a custom theme
- Generating the
Resclass - Troubleshooting common resource generation issues
Creating composeResources
Skip this section if you’re working in a regular Android-only module. Android modules can use the standard src/main/res directory.
If you’re working in a Kotlin Multiplatform or Compose Multiplatform project, create a composeResources directory under commonMain:
shared/
src/
commonMain/
composeResources/
drawable/
font/
values/This makes the resources accessible across the platforms your shared module targets.
After syncing Gradle or running a build, Compose Multiplatform generates a Res object that lets you reference resources like:
Res.font.plusjakartasans_regular
Res.drawable.compose_multiplatformTypography
Defining a Font Family
Browse Google Fonts or another font source and download the font family you want to use.
Place your .ttf font files under:
src/commonMain/composeResources/fontThen sync Gradle or build the project so the generated Res.font accessors are available.
Example:
val PlusJakartaSans
@Composable get() = FontFamily(
Font(
resource = Res.font.plusjakartasans_light,
weight = FontWeight.Light,
),
Font(
resource = Res.font.plusjakartasans_regular,
weight = FontWeight.Normal,
),
Font(
resource = Res.font.plusjakartasans_medium,
weight = FontWeight.Medium,
),
Font(
resource = Res.font.plusjakartasans_semibold,
weight = FontWeight.SemiBold,
),
Font(
resource = Res.font.plusjakartasans_bold,
weight = FontWeight.Bold,
),
)Defining Typography
You can build your app typography using Material 3’s Typography class.
Reference: Material 3 Typography
Avoid naming your app typography value Typography, because that conflicts with the Material 3 Typography type name. A clearer name is AppTypography.
val AppTypography
@Composable get() = Typography(
titleLarge = TextStyle(
fontFamily = PlusJakartaSans,
fontWeight = FontWeight.SemiBold,
fontSize = 30.sp,
lineHeight = 36.sp,
),
titleMedium = TextStyle(
fontFamily = PlusJakartaSans,
fontWeight = FontWeight.SemiBold,
fontSize = 20.sp,
lineHeight = 28.sp,
),
titleSmall = TextStyle(
fontFamily = PlusJakartaSans,
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp,
lineHeight = 24.sp,
),
bodyLarge = TextStyle(
fontFamily = PlusJakartaSans,
fontWeight = FontWeight.Normal,
fontSize = 18.sp,
lineHeight = 26.sp,
),
bodyMedium = TextStyle(
fontFamily = PlusJakartaSans,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
),
bodySmall = TextStyle(
fontFamily = PlusJakartaSans,
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 20.sp,
),
labelMedium = TextStyle(
fontFamily = PlusJakartaSans,
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
lineHeight = 24.sp,
),
labelSmall = TextStyle(
fontFamily = PlusJakartaSans,
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
lineHeight = 20.sp,
),
headlineSmall = TextStyle(
fontFamily = PlusJakartaSans,
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
lineHeight = 18.sp,
),
displaySmall = TextStyle(
fontFamily = PlusJakartaSans,
fontWeight = FontWeight.SemiBold,
fontSize = 11.sp,
lineHeight = 14.sp,
),
)Defining Extra Typography Tokens
If you have typography tokens that are not covered by the Typography constructor, you can expose them as extension properties.
val Typography.labelXSmall: TextStyle
@Composable get() = TextStyle(
fontFamily = PlusJakartaSans,
fontWeight = FontWeight.SemiBold,
fontSize = 11.sp,
lineHeight = 14.sp,
)
val Typography.titleXSmall: TextStyle
@Composable get() = TextStyle(
fontFamily = PlusJakartaSans,
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
lineHeight = 18.sp,
)Usage:
Text(
text = "Small label",
style = MaterialTheme.typography.labelXSmall,
)Color
Gathering Colors
Start by defining your raw color tokens. This makes it easier to understand what each color represents before plugging them into a Material color scheme.
Reference: Material 3 Color System
You do not need to define every Material 3 color role immediately. Start with the core roles your UI needs, then add more as your design system grows.
Defining Color Tokens
// ---------- Light Colors ----------
val LightPrimary = Color(0xFF4F46E5)
val LightOnPrimary = Color(0xFFFFFFFF)
val LightPrimaryContainer = Color(0xFFE0E7FF)
val LightOnPrimaryContainer = Color(0xFF312E81)
val LightSecondary = Color(0xFF06B6D4)
val LightOnSecondary = Color(0xFFFFFFFF)
val LightBackground = Color(0xFFF8FAFC)
val LightOnBackground = Color(0xFF0F172A)
val LightSurface = Color(0xFFFFFFFF)
val LightOnSurface = Color(0xFF1E293B)
val LightError = Color(0xFFDC2626)
val LightOnError = Color(0xFFFFFFFF)
// ---------- Dark Colors ----------
val DarkPrimary = Color(0xFF818CF8)
val DarkOnPrimary = Color(0xFF0F172A)
val DarkPrimaryContainer = Color(0xFF3730A3)
val DarkOnPrimaryContainer = Color(0xFFE0E7FF)
val DarkSecondary = Color(0xFF22D3EE)
val DarkOnSecondary = Color(0xFF082F49)
val DarkBackground = Color(0xFF020617)
val DarkOnBackground = Color(0xFFF8FAFC)
val DarkSurface = Color(0xFF0F172A)
val DarkOnSurface = Color(0xFFE2E8F0)
val DarkError = Color(0xFFF87171)
val DarkOnError = Color(0xFF450A0A)
// ---------- Additional Error Colors ----------
val Red600 = Color(0xFFAA142A)
val Red500 = Color(0xFFDA233E)
val Red200 = Color(0xFFFF7987)
// ---------- Light Theme Success Colors ----------
val LightSuccess = Color(0xFF16A34A)
val LightOnSuccess = Color(0xFFFFFFFF)
val LightSuccessContainer = Color(0xFFDCFCE7)
val LightOnSuccessContainer = Color(0xFF14532D)
// ---------- Dark Theme Success Colors ----------
val DarkSuccess = Color(0xFF4ADE80)
val DarkOnSuccess = Color(0xFF052E16)
val DarkSuccessContainer = Color(0xFF166534)
val DarkOnSuccessContainer = Color(0xFFDCFCE7)Defining Color Schemes
Avoid naming your color scheme values LightColorScheme and DarkColorScheme if you want to make it clear they belong to your app. Prefer names like AppLightColorScheme and AppDarkColorScheme.
val AppLightColorScheme = lightColorScheme(
primary = LightPrimary,
onPrimary = LightOnPrimary,
primaryContainer = LightPrimaryContainer,
onPrimaryContainer = LightOnPrimaryContainer,
secondary = LightSecondary,
onSecondary = LightOnSecondary,
background = LightBackground,
onBackground = LightOnBackground,
surface = LightSurface,
onSurface = LightOnSurface,
error = LightError,
onError = LightOnError,
)
val AppDarkColorScheme = darkColorScheme(
primary = DarkPrimary,
onPrimary = DarkOnPrimary,
primaryContainer = DarkPrimaryContainer,
onPrimaryContainer = DarkOnPrimaryContainer,
secondary = DarkSecondary,
onSecondary = DarkOnSecondary,
background = DarkBackground,
onBackground = DarkOnBackground,
surface = DarkSurface,
onSurface = DarkOnSurface,
error = DarkError,
onError = DarkOnError,
)Defining Extended Colors
Some design tokens do not map directly to Material 3’s built-in ColorScheme roles.
For example, Material 3 has error, but it does not have a built-in success color role. For app-specific roles like success, create an extended color model.
@Immutable
data class ExtendedColors(
val success: Color,
val onSuccess: Color,
val successContainer: Color,
val onSuccessContainer: Color,
)Then define a light and dark version:
val LightExtendedColors = ExtendedColors(
success = LightSuccess,
onSuccess = LightOnSuccess,
successContainer = LightSuccessContainer,
onSuccessContainer = LightOnSuccessContainer,
)
val DarkExtendedColors = ExtendedColors(
success = DarkSuccess,
onSuccess = DarkOnSuccess,
successContainer = DarkSuccessContainer,
onSuccessContainer = DarkOnSuccessContainer,
)Providing Extended Colors with a CompositionLocal
Use a CompositionLocal to provide your extended colors throughout the composable tree.
val LocalExtendedColors = staticCompositionLocalOf {
LightExtendedColors
}A clean way to expose these colors is through an ColorScheme extension:
val ColorScheme.extended: ExtendedColors
@ReadOnlyComposable
@Composable
get() = LocalExtendedColors.currentUsage:
Text(
text = "Success",
color = MaterialTheme.colorScheme.extended.error
)You could also expose extended colors from ColorScheme, but that can be slightly misleading because the extended colors are not actually stored inside ColorScheme; they are provided through LocalExtendedColors.
Defining Your Theme
Create a reusable theme composable that decides whether to use the light or dark color scheme.
@Composable
fun CustomTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
val colorScheme = if (darkTheme) {
AppDarkColorScheme
} else {
AppLightColorScheme
}
val extendedColors = if (darkTheme) {
DarkExtendedColors
} else {
LightExtendedColors
}
CompositionLocalProvider(
LocalExtendedColors provides extendedColors,
) {
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
content = content,
)
}
}Example Usage
CustomTheme {
var showContent by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.primaryContainer)
.safeContentPadding()
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Button(onClick = { showContent = !showContent }) {
Text("Click me!")
}
AnimatedVisibility(showContent) {
val greeting = remember { Greeting().greet() }
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(Res.drawable.compose_multiplatform),
contentDescription = null,
)
Text("Compose: $greeting")
}
}
}
}Generating composeResources
If your Res object is not being generated, update your Gradle setup.
In your module build.gradle.kts, add the Compose resources dependency to commonMain.dependencies:
kotlin {
sourceSets {
commonMain.dependencies {
implementation(compose.components.resources)
}
}
}Then configure resource class generation:
import org.jetbrains.compose.resources.ResourcesExtension.ResourceClassGeneration.Always
compose.resources {
generateResClass = Always
packageOfResClass = "com.yourpackage.generated.resources"
}Replace com.yourpackage.generated.resources with the package where you want the generated Res class to live.
For example:
packageOfResClass = "com.ramzi.theme.generated.resources"Then import it where needed:
import com.ramzi.theme.generated.resources.ResCommon Issues
Unresolved reference: Res
Try the following:
- Confirm your resources are under
src/commonMain/composeResources. - Confirm
implementation(compose.components.resources)is added tocommonMain.dependencies. - Sync Gradle.
- Run a clean build.
- Confirm your
packageOfResClassmatches the import you are using.
Unresolved reference: resources
If this does not resolve:
compose.resources {
generateResClass = Always
}Make sure the Compose Gradle plugin is applied in the module where you are configuring resources.
Example:
plugins {
id("org.jetbrains.compose")
}Depending on your project setup, this may be applied directly in the module or through a convention plugin.
Annotation argument must be a compile-time constant
This usually happens when a generated resource reference is used somewhere that requires a compile-time constant.
Generated Compose resources are not the same as Android R constants. Avoid using Res.font, Res.drawable, or other generated Compose resource values in annotation arguments or places that require compile-time constants.
Font resource is not generated
Check that:
- The file is inside
src/commonMain/composeResources/font. - The file name uses lowercase letters, numbers, and underscores.
- The file extension is supported, such as
.ttf. - Gradle has been synced after adding the file.
- A clean build has been run if the generated accessor still does not appear.
Example valid file name:
plusjakartasans_regular.ttfExample generated reference:
Res.font.plusjakartasans_regular