Styling Your Compose Multiplatform App: A Guide to Fonts and Themes
Hi everyone, I new to KMP development and I started developing my first Compose Multiplatform application. For better understanding and consolidation of covered materials, I decided to write articles about the challenges I will encounter. I think this will also be useful for those who are just getting acquainted with developing multi-platform applications in Kotlin.
So, the first challenge I faced is styling of my Compose application.
How to import fonts to my project? How to set up theme of a project?
First of all, let’s get acquainted with MaterialTheme container.
It acts as a container for your app’s theme and provides access to the current theme settings, such as colors, typography, and shapes.
At start, we need to add Material 3 to build.gradle.kts dependencies. That’s all common compose dependencies we need:
sourceSets {
...
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.components.resources)
}
...
}
Now, let’s see how the MaterialTheme setup should look like:
// Theme.kt
MaterialTheme(
colorScheme = /* ...
typography = /* ...
shapes = /* ...
content = /* ...
)
To theme our application content, we should define the color scheme, typography, and shapes specific to our app. I created “theming” folder and put following files in it:
Typography.
Let’s start from defining typography. Download fonts you need from Google Fonts or wherever and import it. I recommend you to store your fonts in ./composeApp/commonMain/composeResources/fonts
Next, we need to define new Font Family. That’s how I did it:
// Typography.kt
val bonaNovaSC = FontFamily(
Font(Res.font.BonaNovaSC_Bold, FontWeight.Bold),
Font(Res.font.BonaNovaSC_Regular, FontWeight.Normal)
)
Font() is a Composable function, so we need to create @Composable component over it:
// Typography.kt
@Composable
fun AppTypography(): Typography {
// Define a font family for BonaNova SC
val bonaNovaSC = FontFamily(
Font(Res.font.BonaNovaSC_Bold, FontWeight.Bold),
Font(Res.font.BonaNovaSC_Regular, FontWeight.Normal),
)
// Define a font family for DynaPuff
val dynaPuff = FontFamily(
Font(Res.font.DynaPuff_Bold, FontWeight.Bold),
Font(Res.font.DynaPuff_Regular, FontWeight.Normal),
Font(Res.font.DynaPuff_Medium, FontWeight.Medium),
Font(Res.font.DynaPuff_SemiBold, FontWeight.SemiBold),
)
return ...
}
As you can see, we return Typography class from this function. Compose provides this class — along with the existing TextStyle and font-related classes — to model the Material 3 type scale. That’s the final Typography setup:
// Typography.kt
@Composable
fun AppTypography(): Typography {
val bonaNovaSC = FontFamily(
Font(Res.font.BonaNovaSC_Bold, FontWeight.Bold),
Font(Res.font.BonaNovaSC_Regular, FontWeight.Normal),
)
val dynaPuff = FontFamily(
Font(Res.font.DynaPuff_Bold, FontWeight.Bold),
Font(Res.font.DynaPuff_Regular, FontWeight.Normal),
Font(Res.font.DynaPuff_Medium, FontWeight.Medium),
Font(Res.font.DynaPuff_SemiBold, FontWeight.SemiBold),
)
return Typography(
headlineLarge = TextStyle(
fontFamily = dynaPuff,
fontWeight = FontWeight.Bold,
fontSize = 24.sp
),
bodyMedium = TextStyle(
fontFamily = bonaNovaSC,
fontWeight = FontWeight.Normal,
fontSize = 14.sp
),
displaySmall = TextStyle(
fontFamily = dynaPuff,
fontWeight = FontWeight.Medium,
fontSize = 14.sp
)
)
}
Color Scheme.
I have two color schemes for my app. Dark and Light. We define it as Material 3 sets:
// Colors.kt
// Light Theme Colors
val DarkGrayPrimary = Color(0xFF4A4A4A)
val LightGrayButton = Color(0xFFB0B0B0)
val LightBackground = Color(0xFFF8F8F8)
val LightSurface = Color(0xFFFFFFFF)
val LightTextColor = Color(0xFF333333)
// Dark Theme Colors
val WhitePrimary = Color(0xFFFFFFFF)
val DarkGrayButton = Color(0xFF4A4A4A)
val DarkBackground = Color(0xFF1C1C1C)
val DarkSurface = Color(0xFF2A2A2A)
val DarkTextColor = Color(0xFFE0E0E0)
internal val LightColorScheme = lightColorScheme(
primary = DarkGrayPrimary,
background = LightBackground,
onBackground = LightTextColor,
surface = LightSurface,
onSurface = DarkGrayPrimary,
secondary = LightGrayButton,
onSecondary = LightTextColor
)
internal val DarkColorScheme = darkColorScheme(
primary = WhitePrimary,
background = DarkBackground,
onBackground = DarkTextColor,
surface = DarkSurface,
onSurface = WhitePrimary,
secondary = DarkGrayButton,
onSecondary = DarkTextColor
)
Shapes.
It is not neccessary to add shapes, but would be also great:
// Dimensions.kt
val SmallSpacing = 4.dp
val MediumSpacing = 8.dp
val LargeSpacing = 16.dp
// Shapes.kt
val Shapes = Shapes(
small = RoundedCornerShape(SmallSpacing),
medium = RoundedCornerShape(MediumSpacing),
large = RoundedCornerShape(LargeSpacing)
)
Now, let’s create our custom Theme component and add all the sets to it:
// Theme.kt
@Composable
fun Theme(
darkTheme: Boolean = isSystemInDarkTheme(), // Checks if your system is in dark theme mode.
content: @Composable () -> Unit
) {
val colors = if (!darkTheme) DarkColorScheme else LightColorScheme
MaterialTheme(
colorScheme = colors,
typography = AppTypography(),
shapes = Shapes,
content = content,
)
}
Finally, we can apply our component. It’s better to put Surface block inside it, which provides a background for UI elements and helps with elevation:
// App.kt
@Composable
fun App() {
Theme {
Surface (
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
var showContent by remember { mutableStateOf(false) }
val greeting = remember { Greeting().greet() }
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Hello, World!",
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(16.dp),
textAlign = TextAlign.Center
)
Button(
onClick = { showContent = !showContent },
modifier = Modifier.padding(8.dp),
shape = MaterialTheme.shapes.medium
) {
Text(
text = "Click me!",
style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onPrimary
)
}
AnimatedVisibility(visible = showContent) {
Card(
shape = MaterialTheme.shapes.medium,
backgroundColor = MaterialTheme.colorScheme.surface,
modifier = Modifier
.padding(16.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(Res.drawable.compose_multiplatform),
contentDescription = null,
modifier = Modifier.padding(8.dp)
)
Text(
text = "Compose: $greeting",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface
)
}
}
}
}
}
}
}
As you can see, we can apply our sets of fonts and colors and it will adopt to the theme you need. We could also set up Dynamic Color Scheme, but my goal was to establish basic styling in a simple project in a way that’s easy to understand.
Thank you for reading!