Limitations
- Warn the user that this skill is EXPERIMENTAL and requires updating to alpha version of Compose and opting in to the Experimental APIs.
- This skill only supports custom UI components and custom themes.
- This skill does not support Material Design component Styles.
Prerequisites
1. Upgrade dependencies
- The project must use
compileSdkversion 37 or higher. - The project must use
androidx.compose.foundation:foundationversion1.12.0-alpha01or higher. - Alternatively, the project must use Compose BOM version
2026.04.01or higher. - The API requires this exact package:
import androidx.compose.foundation.style.Style
2. Configure compiler options to enable experimental API
You must opt-in to the experimental API at the project level. Add the following
block to your module's build.gradle.kts:
kotlin { compilerOptions { jvmTarget = JvmTarget.fromTarget("17") freeCompilerArgs.add("-opt-in=androidx.compose.foundation.style.ExperimentalFoundationStyleApi") } }
Core workflows and guides
Refer to the official documentation to complete specific development tasks:
- Basic Style Usage: To set backgrounds, sizes, and alignments on a component, follow the Compose Styles Fundamentals Guide.
- State and Transitions: To configure property changes for state shifts (like pressed or hovered), follow the Animations and State-Based Styling Guide.
- Architecture Trade offs: To decide when to use a Style versus a standard Modifier, follow the Styles versus Modifiers Comparison.
- Theme Level Integration: To connect style definitions with custom themes, follow Theming with Styles and Custom Themes in Compose.
Step-by-Step Migration Workflow
Step 1: Analyze theme structure
- Locate your central theme file (such as
Theme.kt). - Identify design tokens. Note references for colors, typography, and shapes (for example,
LocalColorScheme,LocalTypography, orLocalShapes). - If the project lacks Jetpack Compose dependencies, stop. Instruct the user to migrate to Jetpack Compose first.
- If the project imports
androidx.compose.material.MaterialTheme, recommend migrating to Material 3 before proceeding.
Step 2: Establish ComponentStyles
-
Create a new file named
ComponentStyles.ktin your theme directory. -
Define a top-level data class to hold your component styles, for example, the Jetsnack one is called
JetsnackStyles:kotlinobject ExampleComponentStyles { val customButtonStyle: Style = { } val customTextFieldStyle: Style = { } } -
Expose this class through your custom theme with a static reference, don't use
CompositionLocalshere as it's not required.kotlin@Immutable class JetsnackTheme( // other Design system properties ) { companion object { val colors: CustomThemingWithStyles.JetsnackColors @Composable @ReadOnlyComposable get() = LocalJetsnackTheme.current.colors // ... // add helper static reference val styles: ComponentStyles = ComponentStyles } } -
Provide extensions on
StyleScopeto reference theme tokens directly if they are exposed usingCompositionLocals. For example:kotlinval StyleScope.colors: JetsnackColors get() = LocalJetsnackTheme.currentValue.colors val StyleScope.typography: androidx.compose.material3.Typography get() = LocalJetsnackTheme.currentValue.typography val StyleScope.shapes: Shapes get() = LocalJetsnackTheme.currentValue.shapes
Step 3: Migrate a component to Styles API
For each custom component (for example, CustomButton), complete the following
sequence:
- Establish a visual baseline (If an emulator is available):
- If you CANNOT run an Android emulator: Skip this step entirely and proceed to Step 2.
- If you CAN run an Android emulator: Perform the following to capture a baseline screenshot:
- Option A: Locate and run an existing screenshot test for the component.
- Option B (If no test exists): Create a test using the project's existing testing framework, then run it.
- Option C (If no framework exists): Create a minimal screenshot test using UI Automator or Espresso, then run it.
- Remove individual styling parameters : Remove styling parameters such as
backgroundColor,shape,textStyle, andcontentPaddingfrom the signature - anything thatStyleScopesupports. - Add the style parameter : Add
style: Style = Styleto the function signature. Always ensure the default value is exactlyStyle(e.g.,style: Style = Style) and not a specific style default likeChipStyleDefaultor any other value. - Declare state tracking : If the component is interactable, create a
MutableStyleStateusing the interaction source. Update state fields (such asisEnabled) inside the Composable to track the state correctly. - Apply styleable modifier : Replace specific layout modifiers on the root element with
Modifier.styleable(). - Move defaults to ComponentStyles : Move hardcoded values from the component definition to a dedicated
Styleinstance inComponentStyles.kt. - Validate component: Compare the baseline screenshot image taken at the start with the rendered Compose Preview of the new composable. Ignore string content; focus on layout and styling. Iterate on the Compose code until visual parity is achieved. Once verified, write a Compose UI test for the new composable.
Migration example
Before Migration:
kotlin@Composable fun CustomButton( onClick: () -> Unit, modifier: Modifier = Modifier, backgroundColor: Color = JetsnackTheme.colors.brandLight, disabledBackgroundColor: Color = JetsnackTheme.colors.brandSecondary, shape: Shape = JetsnackTheme.shapes.extraLarge, textStyle: TextStyle = JetsnackTheme.typography.labelLarge, enabled: Boolean = true, content: @Composable RowScope.() -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } Row( modifier .clickable(onClick = onClick, indication = null, interactionSource = interactionSource) .background(if (enabled) backgroundColor else disabledBackgroundColor, shape) .defaultMinSize(58.dp, 40.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content, ) }
After Migration:
kotlin// Exposed via ComponentStyles.kt object ComponentStyles { val buttonStyle = Style { background(colors.brandLight) shape(shapes.extraLarge) minWidth(58.dp) minHeight(40.dp) textStyle(typography.labelLarge) disabled { background(colors.brandSecondary) } } } @Composable fun CustomButton( onClick: () -> Unit, modifier: Modifier = Modifier, style: Style = Style, enabled: Boolean = true, content: @Composable RowScope.() -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } val styleState = rememberUpdatedStyleState(interactionSource) { it.isEnabled = enabled } Row( modifier .clickable(onClick = onClick, indication = null, interactionSource = interactionSource) .styleable(styleState, JetsnackTheme.styles.buttonStyle, style), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content, ) }
Step 4: Validate Changes
- Build the project. Verify that there are no compilation errors.
- Run your module's screenshot tests.
- Compare visual outputs of the whole app between the previous and updated components. Verify that no visual layout regressions occur.

