Compose Unstyled v1.49: Introducing Platform Themes

Alex Styl@alexstyl

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:

ScaleAndroidiOSDesktopWeb
111sp12sp10sp10sp
212sp13sp11sp12sp
314sp16sp12sp14sp
4 (base)16sp17sp13sp16sp
522sp18sp14sp18sp
624sp20sp15sp20sp
728sp22sp17sp24sp
832sp28sp22sp28sp
936sp34sp26sp35sp

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.

TokenAndroidiOSDesktopWeb
sizeDefault48dp44dp28dp28dp
sizeMinimum32dp28dp20dp20dp

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.

TokenRadius
roundedNone0dp
roundedSmall4dp
roundedMedium6dp
roundedLarge8dp
roundedFull100%
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 🫑


Stay updated with new updates like this as soon as we ship them