Posted by Michael Stillwell Developer Relations Engineer
This post is part of Wear OS Spotlight Week. Today, we’re focusing on creating engaging Wear OS tiles with new resources and updated design guidance.
Wear OS is all about providing users with the right information at the right time. While a full app is great for immersive experiences, sometimes users just need a quick glance at information or a simple way to take action.
Tiles are fast, predictable surfaces that users can access with a simple swipe from their watch face. They are designed for quick, frequent tasks like checking the weather, starting a timer, or tracking fitness goals.
To streamline your workflow from concept to launch, we’re pleased to announce a collection of resources to help you build beautiful and effective tiles using the Material 3 Expressive design system.
- For designers, see the design guidance for tiles, tile layouts and the Figma design kit. These resources help you craft expressive, modern tiles that feel like a cohesive and premium part of the Wear OS experience.
- For developers, see the official documentation, as well as the code sample on GitHub — now with a ready-to-use repository of sample layout implementations updated for Material 3 Expressive.
These layouts are an evolution of a previous version based on Material 2.5, now updated to Material 3 Expressive to create a modern, premium feel that makes your tile a more cohesive part of the Wear OS.

We hope these resources serve as inspiration and a practical starting point, whether you’re new to Wear OS development or looking to add a tile to your existing app.
Get started with sample layouts
Tiles are built declaratively using the ProtoLayout library. Material 3 Expressive’s Primary Layout is organized around a slot-based architecture. Running from top to bottom, these slots are:
- An optional titleSlot for a header.
- A mandatory mainSlot for your core content.
- An optional bottomSlot for supplemental actions.
Your app implements a TileService, which returns a layout when requested by the system. This layout is then used to build and render the tile. Learn how to get started with tiles.
As an example, here’s the “Goal” layout for visualizing step count. The titleSlot contains the “Steps” text, the mainSlot holds the graphic data card with a progress ring and step data, and the bottomSlot features the “Track” edge-hugging button. (The icon that appears at the top of the tile is specified in your app’s manifest is and drawn by the system.)

The code for this layout is structured logically around these slots:
fun layout( context: Context, deviceParameters: DeviceParameters, steps: Int, goal: Int ) = materialScope( context = context, deviceConfiguration = deviceParameters ) { val stepsString = NumberFormat.getNumberInstance().format(steps) val goalString = NumberFormat.getNumberInstance().format(goal) primaryLayout( titleSlot = { text("Steps".layoutString) }, // Adjust margins to create more space when using fully rounded corners margins = PrimaryLayoutMargins.MIN_PRIMARY_LAYOUT_MARGIN, mainSlot = { graphicDataCard( onClick = clickable(), height = expand(), colors = filledTonalCardColors(), title = { text(stepsString.layoutString) }, content = { text("of $goalString".layoutString) }, horizontalAlignment = LayoutElementBuilders.HORIZONTAL_ALIGN_END, graphic = { constructGraphic( mainContent = { circularProgressIndicator( staticProgress = 1F * steps / goal, // On supported devices, animate the arc dynamicProgress = DynamicFloat.onCondition( PlatformEventSources.isLayoutVisible() ) .use(1F * data.steps / data.goal) .elseUse(0F) .animate( CircularProgressIndicatorDefaults .recommendedAnimationSpec ), startAngleDegrees = 200F, endAngleDegrees = 520F, ) }, iconContent = { icon(ICON_ID) }, ) }, ) }, bottomSlot = { textEdgeButton(onClick = clickable()) { text("Track".layoutString) } }, ) }
With this simple function, you get a great-looking, responsive tile. The ProtoLayout Material3 library handles the heavy lifting, such as setting margins to avoid clipping on round screens and ensuring components adapt smoothly to different display sizes.
Create custom tile layouts
While our layouts cover many common use cases, you’ll sometimes need a unique layout. The Material 3 Expressive components provide a flexible foundation for building custom designs.
To translate designs into code, start with the most visually similar layout and modify it. The following sections explain how to modify an existing layout slot by slot.
Customize the title and bottom slots
The titleSlot is often a text() element. To verify that the tap targets of the other elements are interactive, you may wish to hide the title slot on smaller devices. Learn how to develop tiles for different screen sizes.
primaryLayout( titleSlot = if (isLargeScreen()) { { text("$tasksLeft mindful tasks left".layoutString) } } else { null }, // ... )
The bottomSlot provides users with a primary action, typically an EdgeButton. You can use a textEdgeButton() for a descriptive action. Alternatively, you can use an icon such as + by using an iconEdgeButton.
Using an icon is a two-step process:
- Define the iconEdgeButton in your layout, giving your icon a unique resource ID string:
- Provide the actual drawable resource in onTileResourcesRequest():
primaryLayout( // ... bottomSlot = { iconEdgeButton( onClick = clickable(), modifier = LayoutModifier.contentDescription("Add event"), iconContent = { icon("icon_plus_id") } ) } )
override fun onTileResourcesRequest( requestParams: RequestBuilders.ResourcesRequest ) = Futures.immediateFuture( ResourceBuilders.Resources.Builder() .setVersion(requestParams.version) .addIdToImageMapping( "plus_icon_id", ResourceBuilders.ImageResource.Builder() .setAndroidResourceByResId( ResourceBuilders.AndroidImageResourceByResId.Builder() .setResourceId(R.drawable.outline_add_2_24) .build() ) .build() ) .build() )
See Alarm.kt for a full code sample demonstrating this approach.
Customize the main slot
The mainSlot is where the core content of your tile lives and where the most significant customization occurs. Let’s walk through a few examples.
Case study: Workout tile


This tile needs to adapt its layout for different screen sizes. For the smaller layout, three simple iconButton components are a perfect fit. In the larger layout, the central button displays more data (duration, unit, and an icon). Even though it’s semantically still a button, in this case the iconDataCard element is a better fit. It’s specifically designed to display multiple pieces of data, and we can easily adjust its width and height.
iconDataCard( title = { text("30".layoutString, typography = DISPLAY_MEDIUM) }, content = { text("Mins".layoutString, typography = TITLE_MEDIUM) }, secondaryIcon = { icon("icon_run_id") }, shape = shapes.large, // adjust the corner shape onClick = clickable(), // make element more prominent on larger screens width = if (isLargeScreen()) weight(1.5f) else expand(), height = expand(), // ... )
See Workout.kt for the full source code.
Case study: Skiing stats tile

The design for this tile is built around a pill-shaped element that displays three lines of text, each with unique typography. A textDataCard() is perfect for this, offering slots for a “title” (the metric), “content” (the value), and “secondaryText” (the units). These slots come with default styling that you can override to match your design precisely.
fun MaterialScope.statTextButton(stat: Stat) = textDataCard( onClick = clickable(), width = expand(), height = expand(), shape = shapes.extraLarge, title = { text( stat.value.layoutString, typography = if (isLargeScreen()) { Typography.NUMERAL_SMALL } else { Typography.NUMERAL_EXTRA_SMALL } ) }, content = { text( stat.unit.layoutString, typography = if (isLargeScreen()) { Typography.TITLE_MEDIUM } else { Typography.TITLE_SMALL } ) }, secondaryText = { text( stat.label.layoutString, typography = if (isLargeScreen()) { Typography.TITLE_MEDIUM } else { Typography.TITLE_SMALL } ) } )
Notice how the typography constants are varied according to the screen size (TITLE_MEDIUM vs. TITLE_SMALL, for example). Given the variety of screen and font sizes on Wear OS, this is a key technique to keep text legible. Instead of trying to manually tweak your layout for every possible combination, focus on adjusting the typography by using different font size constants.
For a consistent look, try to stick to the same “category” of typography constant, such as BODY_MEDIUM on small screens and BODY_LARGE on larger ones. Avoid jumping between different categories, like from LABEL_LARGE to DISPLAY_SMALL, as these constants can vary in more than just size, affecting font weight and other visual properties.
See Ski.kt for the full source code.
Another approach to adapting a layout to different screen sizes is simply to add or remove elements depending on the display size, as demonstrated by the Weather.kt layout. While both versions display the same current conditions, the larger version adds more information to the forecast.


Customize colors
You might notice that the templates don’t specify a color scheme, yet they adapt to the user’s chosen theme on Wear OS 6. This is due to dynamic theming, a system feature that automatically generates a color scheme by extracting seed colors from sources like the user’s watch face. For tiles, this is the default behavior.

As a developer, this gives you two main options for your tile’s appearance:
Option 1 (recommended): Follow dynamic color theming. A dynamic theme is used by default. In this case, you should provide a defaultColorScheme to be used as a fallback if the user disables dynamic theming or if the device doesn’t support it. This approach creates a consistent and cohesive feel, allowing your tile to integrate seamlessly with the system.
val myColorScheme = ColorScheme( primary = ... onPrimary = ... // 27 more ) materialScope( defaultColorScheme = myColorScheme ) { // If the user selects "no theme" in settings, myColorScheme is used. // Otherwise, the system-provided theme is used. }
Option 2: Use your brand colors. To ensure brand consistency, you can force your tile to always use your custom color scheme by setting allowDynamicTheme to false. In this case, the same colors will always be used, irrespective of the user’s selected color theme.
materialScope( allowDynamicTheme = false, defaultColorScheme = myColorScheme ) { // myColorScheme is *always* used. }
See Theming for more information on the theming system.
Develop and debug
To speed up your development cycle, Wear OS provides several tools to help you debug and test your tiles directly in Android Studio and on the command line.
- Preview tiles in Android Studio. This is especially helpful when developing layouts for different screen sizes.
- Manipulate tiles with adb. Useful for QA tasks, including screenshot testing.
Dive in and start building
These resources are designed to make building high-quality Wear OS tiles easier and more inspiring. We can’t wait to see what you create with them.
Explore the layouts and get started today by checking out the Figma design kit or the code on GitHub.