Compose Unstyled 2.0 is out! Check the official announcement blog ->

A slider component with custom track and thumb slots.

import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.hoverable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.composeunstyled.UnstyledSlider

@Composable
fun SliderDemo() {
  Box(
    modifier = Modifier.fillMaxSize(),
    contentAlignment = Alignment.Center,
  ) {
    val interactionSource = remember { MutableInteractionSource() }
    val isFocused by interactionSource.collectIsFocusedAsState()
    val isPressed by interactionSource.collectIsPressedAsState()

    var value by remember { mutableFloatStateOf(0.7f) }

    Box(
      modifier = Modifier
        .padding(horizontal = 16.dp)
        .widthIn(max = 480.dp)
        .fillMaxWidth(),
    ) {
      UnstyledSlider(
        interactionSource = interactionSource,
        value = value,
        onValueChange = { value = it },
        modifier = Modifier.fillMaxWidth(),
        track = { state ->
          Box(
            Modifier
              .fillMaxWidth()
              .height(8.dp)
              .padding(horizontal = 16.dp)
              .clip(RoundedCornerShape(100.dp)),
          ) {
            // the 'not yet completed' part of the track
            Box(
              Modifier
                .fillMaxHeight()
                .fillMaxWidth()
                .background(Color(0xFFCACACA)),
            )
            // the 'completed' part of the track
            Box(
              Modifier
                .fillMaxHeight()
                .fillMaxWidth(state.fraction)
                .background(Color.Black),
            )
          }
        },
        thumb = {
          val thumbSize by animateDpAsState(targetValue = if (isPressed) 22.dp else 18.dp)

          val thumbInteractionSource = remember { MutableInteractionSource() }
          val isHovered by thumbInteractionSource.collectIsHoveredAsState()
          val glowColor by animateColorAsState(
            if (isFocused || isHovered) Color.Black.copy(0.16f) else Color.Transparent,
          )
          // keep the size fixed to ensure that the resizing animation is always centered
          Box(
            modifier = Modifier.size(36.dp).clip(CircleShape).background(glowColor),
            contentAlignment = Alignment.Center,
          ) {
            Box(
              modifier = Modifier
                .size(thumbSize)
                .hoverable(thumbInteractionSource)
                .clip(CircleShape)
                .background(Color.Black),
            )
          }
        },
      )
    }
  }
}

Features

  • Horizontal and vertical sliders
  • Discrete step support
  • Custom track and thumb slots
  • Keyboard and screen reader value changes

Installation

implementation("com.composables:composeunstyled-slider")

Anatomy

UnstyledSlider(
  value = value,
  onValueChange = onValueChange,
  track = {
  },
  thumb = {
  },
)

Concepts

  • UnstyledSlider represents the interactive range users can drag or adjust.
  • SliderState is passed to the track and thumb slots.
  • The track slot renders below the thumb.
  • The thumb slot renders at the current slider offset.

Accessibility

UnstyledSlider exposes progress semantics and supports arrow keys, Page Up, Page Down, Home, and End.

Code Examples

Creating a stepped slider

Use the steps parameter to snap the slider value to discrete stops:

UnstyledSlider(
  value = value,
  onValueChange = { value = it },
  steps = 4,
  track = { state ->
    BasicText("${state.value}")
  },
  thumb = { state ->
    BasicText("${state.value}")
  },
)

Using a custom value range

Use the valueRange parameter when the slider value is not from 0f to 1f:

UnstyledSlider(
  value = volume,
  onValueChange = { volume = it },
  valueRange = 0f..100f,
  track = { state ->
    BasicText("${state.value}")
  },
  thumb = { state ->
    BasicText("${state.value}")
  },
)

Creating a vertical slider

Use the orientation parameter to make the slider vertical:

UnstyledSlider(
  value = value,
  onValueChange = { value = it },
  orientation = Orientation.Vertical,
  track = { state ->
    BasicText("${state.value}")
  },
  thumb = { state ->
    BasicText("${state.value}")
  },
)

Running code after value changes finish

Use the onValueChangeFinished callback to react after drag, tap, keyboard, or screen reader changes finish:

UnstyledSlider(
  value = value,
  onValueChange = { value = it },
  onValueChangeFinished = { save(value) },
  track = { state ->
    BasicText("${state.value}")
  },
  thumb = { state ->
    BasicText("${state.value}")
  },
)

API Reference

SliderState

Parameter Type Description
valueRange ClosedFloatingPointRange<Float> The range of values the slider can take.
steps Int The number of discrete steps in the slider.
enabled Boolean Whether the slider can receive user input.
orientation Orientation Horizontal or vertical slider orientation.
isRtl Boolean
isDragging Boolean
isPressed Boolean
isFocused Boolean
tickFractions FloatArray
value Float The current value of the slider.
fraction Float

UnstyledSlider

Parameter Type Description
value Float The controlled value of the slider.
onValueChange (Float) -> Unit Callback invoked when the user changes the value.
modifier Modifier Modifier to be applied to the slider.
enabled Boolean Whether the slider can receive user input.
interactionSource MutableInteractionSource? Interaction source for press, focus, and drag interactions.
valueRange ClosedFloatingPointRange<Float> The range of values the slider can take.
steps Int The number of discrete stops between the start and end.
onValueChangeFinished (() -> Unit)?
orientation Orientation Horizontal or vertical slider orientation.
reverseDirection Boolean Whether to reverse the visual and input direction.
track (SliderState) -> Unit Composable function to define the track of the slider.
thumb (SliderState) -> Unit Composable function to define the thumb of the slider.