Have you tried Compose on iOS and felt icky because of the default touch animation when pressing on buttons and elements?
On Android you have this beautiful Material ripple going on, but on iOS there is this ugly gray touch effect that just destroys the quality of your app.
That is a problem of the past with the introduction of Platform Themes. You now get beautiful defaults on every platform such as touch indications, beautiful typography and emojis out of the box on Web.
Get started
To use platform themes, add the optional composeunstyled-platformtheme module to your dependencies:
implementation("com.composables:composeunstyled-platformtheme:1.49.1")
Using Platform Themes
To create a platform theme call buildPlatformTheme() and then use the returned composable as your app's theme.
You can then use the related theme tokens to style your app.
val AppTheme = buildPlatformTheme(
webFontOptions = WebFontOptions(
emojiVariant = EmojiVariant.Colored
)
)
@Composable
fun App() {
AppTheme {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("π₯°βοΈπ’π", style = Theme[textStyles][text8])
Text(
text = "Beautiful styling defaults on every platform",
style = Theme[textStyles][heading5]
)
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Button(
onClick = { },
contentPadding = PaddingValues(
horizontal = 16.dp, vertical = 8.dp
),
shape = Theme[shapes][roundedFull],
backgroundColor = Color(0xFF3B82F6),
indication = Theme[indications][dimmed],
modifier = Modifier
.interactiveSize(Theme[interactiveSizes][sizeDefault])
) {
Text("Get Started", color = Color.White)
}
}
}
}
}
which renders the following:
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
There is a lot of stuff you get for free here so let's break it down.
Native interaction effects
Platform Themes apply an indication to their children according to each platform's look and feel.
val AppTheme = buildPlatformTheme()
@Composable
fun App() {
AppTheme {
Button(
onClick = {},
modifier = Modifier.size(90.dp).shadow(4.dp, RoundedCornerShape(12.dp)),
backgroundColor = Color.White,
shape = RoundedCornerShape(12.dp),
indication = Theme[indications][dimmed]
) {
}
}
}
Components such as Button and clickable() elements will use this indication. The final effect depends on the platform you are running on:
Android
iOS
Desktop
Web
The default animation will brighten the element when interacted with. You can use the bright and dimmed theme tokens to control the effect.
Effortless native typography on every platform
Just by using a Platform Theme all your Text and TextField composables are automatically styled according to each
platform's design guidelines.
Your app will use each platform's native font family (except Web, but more on that in a moment).
On top of that, platform themes adapt the typography sizing to ensure comfortable reading out of the box without looking oversized on desktop or tiny on mobile.
This sizing comes from each platform's design guidelines, such as Material on Android and Human Interface Guidelines on Apple devices:
| Scale | Android | iOS | Desktop | Web |
|---|---|---|---|---|
1 | 11sp | 12sp | 10sp | 10sp |
2 | 12sp | 13sp | 11sp | 12sp |
3 | 14sp | 16sp | 12sp | 14sp |
4 (base) | 16sp | 17sp | 13sp | 16sp |
5 | 22sp | 18sp | 14sp | 18sp |
6 | 24sp | 20sp | 15sp | 20sp |
7 | 28sp | 22sp | 17sp | 24sp |
8 | 32sp | 28sp | 22sp | 28sp |
9 | 36sp | 34sp | 26sp | 35sp |
By default, every text will have the size of 4. You can use this scale in your apps using the text and heading
theme tokens:
Text("Title", style = Theme[textStyles][heading5])
Text("Body", style = Theme[textStyles][text4])
Beautiful Typography on Web
Anyone who used Compose on Web has struggled with rendering non-latin text in their apps.
This is because there is no browser API to access installed fonts like there is via CSS.
To make everyone's life easier, Platform Themes come bundled with Noto Sans on Web. Noto Sans is a universal font that comes with scripts for pretty much every language.
Use webFontOptions when building your Theme to specify the scripts that your app needs.
Here is how to render Japanese in Compose Web:
val AppTheme = buildPlatformTheme(
webFontOptions = WebFontOptions(
supportedLanguages = listOf(SpokenLanguage.Japanese)
)
)
@Composable
fun App() {
AppTheme {
Text("ζ΅·θ³ηγ«δΏΊγ―γͺγ", style = Theme[textStyles][heading5])
}
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
There is currently support for Japanese, Korean, Chinese Traditional and Chinese Simplified. If there is a script you would like to be supported, feel free to request it via a GitHub issue.
Emojis on Web out of the box
Platform Themes come with emojis out of the box on web.
By default, emojis are monochrome as it's the best compromise between loading speed and sensible defaults.
You can enable colored emojis or disable them completely via the respective option:
val AppTheme = buildPlatformTheme(
webFontOptions = WebFontOptions(
emojiVariant = EmojiVariant.Colored
)
)
@Composable
fun App() {
AppTheme {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text("π π β€οΈ π π¨", style = Theme[textStyles][heading8])
}
}
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
Interaction sizes
Platform Themes provide interaction size tokens that ensure your interactive elements meet accessibility standards on every platform. The sizing comes from each platform's design guidelines, ensuring optimal usability whether users are tapping on a touchscreen or clicking with a mouse.
| Token | Android | iOS | Desktop | Web |
|---|---|---|---|---|
sizeDefault | 48dp | 44dp | 28dp | 28dp |
sizeMinimum | 32dp | 28dp | 20dp | 20dp |
Use the interactiveSize modifier to apply these sizes to your interactive elements:
import com.composeunstyled.Button
import com.composeunstyled.Text
import com.composeunstyled.platformtheme.interactiveSize
import com.composeunstyled.platformtheme.interactiveSizes
import com.composeunstyled.platformtheme.sizeDefault
import com.composeunstyled.theme.Theme
Button(
onClick = { /* ... */ },
modifier = Modifier.interactiveSize(Theme[interactiveSizes][sizeDefault])
) {
Text("Click me")
}
Shapes
Shape theme tokens are not platform-specific; however, they are very handy when building apps.
| Token | Radius |
|---|---|
roundedNone | 0dp |
roundedSmall | 4dp |
roundedMedium | 6dp |
roundedLarge | 8dp |
roundedFull | 100% |
val AppTheme = buildPlatformTheme()
AppTheme {
Box(modifier = Modifier.size(60.dp).background(Color(0xFF3B82F6), Theme[shapes][roundedNone]))
Box(modifier = Modifier.size(60.dp).background(Color(0xFF3B82F6), Theme[shapes][roundedSmall]))
Box(modifier = Modifier.size(60.dp).background(Color(0xFF3B82F6), Theme[shapes][roundedMedium]))
Box(modifier = Modifier.size(60.dp).background(Color(0xFF3B82F6), Theme[shapes][roundedLarge]))
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
Modularization
Last but not least, I heard your feedback about Unstyle's API surface growing big and this is something I could finally address with this release.
The library has been split into smaller modules so you can pick only the ones you need for your team and projects:
// adds all APIs (recommended for bootstrapping new projects)
implementation("com.composables:composeunstyled:1.49.1")
// adds theming APIs
implementation("com.composables:composeunstyled-theming:1.49.1")
// adds component primitives for building components
implementation("com.composables:composeunstyled-primitives:1.49.1")
// adds themes for native look and feel
implementation("com.composables:composeunstyled-platformtheme:1.49.1")
That's all about today's update. Hope it's helpful in building premium-looking multiplatform apps.
Have a great weekend π«‘

