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:object 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.@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:val 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:
- If you are able to run an Android emulator, locate an existing screenshot test for the component. If none exists, create one using the existing project testing framework. If no framework exists, use UI Automator or Espresso to create a screenshot test with minimum required setup. Run the test and take a baseline screenshot of the Component. ELSE proceed to the next step without a screenshot test.
- 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. - 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:
@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:
// 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.