The Styles API offers a declarative and streamlined approach to managing UI
changes during interaction states like hovered, focused, and pressed. With
this API, you can significantly decrease the boilerplate code typically required
when using modifiers.
To facilitate reactive styling, StyleState acts as a stable, read-only
interface that tracks the active state of an element (such as its enabled,
pressed, or focused status). Within a StyleScope, you can access this through
the state property to implement conditional logic directly in your Style
definitions.
State-based interaction: Hovered, focused, pressed, selected, enabled, toggled
Styles come with built-in support for common interactions:
- Pressed
- Hovered
- Selected
- Enabled
- Toggled
It's also possible to support custom states. See the Custom State Styling with StyleState section for more information.
Handle interaction states with Style parameters
The following example demonstrates modifying the background and borderColor
in response to interaction states, specifically switching to purple when hovered
and blue when focused:
@Preview @Composable private fun OpenButton() { BaseButton( style = outlinedButtonStyle then { background(Color.White) hovered { background(lightPurple) border(2.dp, lightPurple) } focused { background(lightBlue) } }, onClick = { }, content = { BaseText("Open in Studio", style = { contentColor(Color.Black) fontSize(26.sp) textAlign(TextAlign.Center) }) } ) }
You can also create nested state definitions. For example, you can define a specific style for when a button is being both pressed and hovered simultaneously:
@Composable private fun OpenButton_CombinedStates() { BaseButton( style = outlinedButtonStyle then { background(Color.White) hovered { // light purple background(lightPurple) pressed { // When running on a device that can hover, whilst hovering and then pressing the button this would be invoked background(lightOrange) } } pressed { // when running on a device without a mouse attached, this would be invoked as you wouldn't be in a hovered state only background(lightRed) } focused { background(lightBlue) } }, onClick = { }, content = { BaseText("Open in Studio", style = { contentColor(Color.Black) fontSize(26.sp) textAlign(TextAlign.Center) }) } ) }
Custom composables with Modifier.styleable
When creating your own styleable components, you must connect an
interactionSource to a styleState. Then, pass this state into
Modifier.styleable to utilize it.
Consider a scenario where your design system includes a GradientButton. You
may want to create a LoginButton that inherits from GradientButton, but
alters its colors during interactions, like being pressed.
- To enable
interactionSourcestyle updates, include aninteractionSourceas a parameter within your composable. Use the provided parameter or, if one is not supplied, initialize a newMutableInteractionSource. - Initialize the
styleStateby providing theinteractionSource. Make sure thestyleState's enabled status reflects the value of the provided enabled parameter. - Assign the
interactionSourceto thefocusableandclickablemodifiers. Finally, apply thestyleStateto the modifier'sstyleableparameter.
@Composable private fun GradientButton( onClick: () -> Unit, modifier: Modifier = Modifier, style: Style = Style, enabled: Boolean = true, interactionSource: MutableInteractionSource? = null, content: @Composable RowScope.() -> Unit, ) { val interactionSource = interactionSource ?: remember { MutableInteractionSource() } val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } styleState.isEnabled = enabled Row( modifier = modifier .clickable( onClick = onClick, enabled = enabled, interactionSource = interactionSource, indication = null, ) .styleable(styleState, baseGradientButtonStyle then style), content = content, ) }
You can now use the interactionSource state to drive style modifications with
the pressed, focused, and hovered options inside the style block:
@Preview @Composable fun LoginButton() { val loginButtonStyle = Style { pressed { background( Brush.linearGradient( listOf(Color.Magenta, Color.Red) ) ) } } GradientButton(onClick = { // Login logic }, style = loginButtonStyle) { BaseText("Login") } }
interactionSource.Animate style changes
Styles state changes come with built-in animation support. You can wrap the new
property within any state change block with animate to automatically add
animations between different states. This is similar to the animate*AsState
APIs. The following example animates the borderColor from black to blue when
the state changes to focused:
val animatingStyle = Style { externalPadding(48.dp) border(3.dp, Color.Black) background(Color.White) size(100.dp) pressed { animate { borderColor(Color.Magenta) background(Color(0xFFB39DDB)) } } } @Preview @Composable private fun AnimatingStyleChanges() { val interactionSource = remember { MutableInteractionSource() } val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } Box(modifier = Modifier .clickable( interactionSource, enabled = true, indication = null, onClick = { } ) .styleable(styleState, animatingStyle)) { } }
The animate API accepts an animationSpec to change the duration or shape of
the animation curve. The following example animates the size of the box with a
spring spec:
val animatingStyleSpec = Style { externalPadding(48.dp) border(3.dp, Color.Black) background(Color.White) size(100.dp) transformOrigin(TransformOrigin.Center) pressed { animate { borderColor(Color.Magenta) background(Color(0xFFB39DDB)) } animate(spring(dampingRatio = Spring.DampingRatioMediumBouncy)) { scale(1.2f) } } } @Preview(showBackground = true) @Composable fun AnimatingStyleChangesSpec() { val interactionSource = remember { MutableInteractionSource() } val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } Box(modifier = Modifier .clickable( interactionSource, enabled = true, indication = null, onClick = { } ) .styleable(styleState, animatingStyleSpec)) }
Custom state styling with StyleState
Depending on your composable use case, you may have different styles that are
backed by custom states. For example, if you have a media app, you may want to
have different styling for the buttons in your MediaPlayer composable
depending on the playback state of the player. Follow these steps to create and
use your own custom state:
- Define custom key
- Create
StyleStateextension - Link to custom state
Define custom key
To create a custom state-based style, first create a
StyleStateKey and pass in the default state value. When the
app launches, the media player is in the Stopped state, so it's initialized in
this way:
enum class PlayerState { Stopped, Playing, Paused } val playerStateKey = StyleStateKey(PlayerState.Stopped)
Create StyleState extension functions
Define an extension function on StyleState to query the current playState.
Then, create extension functions on StyleScope with your custom states passing
in the playStateKey, a lambda with the specific state, and the style.
// Extension Function on MutableStyleState to query and set the current playState var MutableStyleState.playerState get() = this[playerStateKey] set(value) { this[playerStateKey] = value } fun StyleScope.playerPlaying(value: Style) { state(playerStateKey, value, { key, state -> state[key] == PlayerState.Playing }) } fun StyleScope.playerPaused(value: Style) { state(playerStateKey, value, { key, state -> state[key] == PlayerState.Paused }) }
Link to custom state
Define the styleState in your composable and set the styleState.playState
equal to incoming state. Pass styleState into the styleable function on the
modifier.
@Composable fun MediaPlayer( url: String, modifier: Modifier = Modifier, style: Style = Style, state: PlayerState = remember { PlayerState.Paused } ) { // Hoist style state, set playstate as a parameter, val styleState = remember { MutableStyleState(null) } // Set equal to incoming state to link the two together styleState.playerState = state Box( modifier = modifier.styleable(styleState, style)) { ///.. } }
Within the style lambda, you can apply state based styling for custom states,
using the previously defined extension functions.
@Composable fun StyleStateKeySample() { // Using the extension function to change the border color to green while playing val style = Style { borderColor(Color.Gray) playerPlaying { animate { borderColor(Color.Green) } } playerPaused { animate { borderColor(Color.Blue) } } } val styleState = remember { MutableStyleState(null) } styleState[playerStateKey] = PlayerState.Playing // Using the style in a composable that sets the state -> notice if you change the state parameter, the style changes. You can link this up to an ViewModel and change the state from there too. MediaPlayer(url = "https://example.com/media/video", style = style, state = PlayerState.Stopped) }
The following code is the full snippet for this example:
enum class PlayerState { Stopped, Playing, Paused } val playerStateKey = StyleStateKey<PlayerState>(PlayerState.Stopped) var MutableStyleState.playerState get() = this[playerStateKey] set(value) { this[playerStateKey] = value } fun StyleScope.playerPlaying(value: Style) { state(playerStateKey, value, { key, state -> state[key] == PlayerState.Playing }) } fun StyleScope.playerPaused(value: Style) { state(playerStateKey, value, { key, state -> state[key] == PlayerState.Paused }) } @Composable fun MediaPlayer( url: String, modifier: Modifier = Modifier, style: Style = Style, state: PlayerState = remember { PlayerState.Paused } ) { // Hoist style state, set playstate as a parameter, val styleState = remember { MutableStyleState(null) } // Set equal to incoming state to link the two together styleState.playerState = state Box( modifier = modifier.styleable(styleState, Style { size(100.dp) border(2.dp, Color.Red) }, style, )) { ///.. } } @Composable fun StyleStateKeySample() { // Using the extension function to change the border color to green while playing val style = Style { borderColor(Color.Gray) playerPlaying { animate { borderColor(Color.Green) } } playerPaused { animate { borderColor(Color.Blue) } } } val styleState = remember { MutableStyleState(null) } styleState[playerStateKey] = PlayerState.Playing // Using the style in a composable that sets the state -> notice if you change the state parameter, the style changes. You can link this up to an ViewModel and change the state from there too. MediaPlayer(url = "https://example.com/media/video", style = style, state = PlayerState.Stopped) }