Package-level declarations

Steppers let users increment and decrement a numeric value within a defined range using decrease/increase buttons on either side of the current value.

All stepper variants live inside object Stepper and come in two flavours: Nudger (read-only display) and Input (editable text field).

Stepper.Nudger

The Nudger variant displays the value between decrease/increase buttons. The value is read-only; users change it via the buttons. Each digit animates independently on change (odometer effect). Long-pressing a button repeats the action with acceleration.

var quantity by rememberSaveable { mutableStateOf<Int?>(0) }

Stepper.Nudger(
value = quantity,
onValueChange = { quantity = it },
range = 1..99,
step = 1,
)

Nullable value

Pass null for an empty initial state. Pressing either button snaps to range.first.

var quantity by rememberSaveable { mutableStateOf<Int?>(null) }

Stepper.Nudger(
value = quantity,
onValueChange = { quantity = it },
range = 0..10,
)

State holder

StepperState bundles value, range, and step into a single saveable holder.

val state = rememberStepperState(initialValue = 5, range = 0..100, step = 5)

Stepper.Nudger(state = state)

Parameters

ParameterDefaultDescription
value-Current displayed value, or null for empty
onValueChange-Called after each increment or decrement
range0..10Accepted value bounds; buttons disable at the limits
step1Amount added or subtracted per button press
suffix""String appended after the value, e.g. " kg"
placeholder"-"Text shown in the empty state when value is null
enabledtrueDisables interaction and applies disabled styling
flexiblefalseWhen true, fills maximum available width

Stepper.NudgerForm

Wraps Stepper.Nudger with a label, optional helper text, and status message.

var guests by rememberSaveable { mutableStateOf<Int?>(1) }

Stepper.NudgerForm(
value = guests,
onValueChange = { guests = it },
label = "Number of guests",
helper = "Maximum 8 per booking",
range = 1..8,
required = true,
status = FormFieldStatus.Error,
statusMessage = "Exceeds maximum capacity",
)

State holder

val state = rememberStepperState(initialValue = 1, range = 1..8, step = 1)

Stepper.NudgerForm(
state = state,
label = "Number of guests",
helper = "Maximum 8 per booking",
)

Additional parameters

ParameterDescription
labelField label displayed above the stepper
helperHint text shown below when status is null
requiredAppends * to the label and reads it as "mandatory field"
statusValidation state; tints the helper text
statusMessageReplaces helper when status is non-null

Stepper.Input

The Input variant places an editable text field between the buttons. Users can type a value directly or use the buttons. Non-digit characters are rejected, and the value is clamped to the range on blur. An empty field has value == null; the field then shows the placeholder, which defaults to -. Long-pressing a button repeats with acceleration.

var quantity by rememberSaveable { mutableStateOf<Int?>(3) }

Stepper.Input(
value = quantity,
onValueChange = { quantity = it },
range = 0..100,
step = 1,
)

State holder

StepperInputState wraps a TextFieldState and exposes a parsed value: Int?.

val state = rememberStepperInputState(initialValue = 5)

Stepper.Input(
state = state,
range = 0..100,
step = 1,
)
Text(text = "Current value: ${state.value ?: "empty"}")

Parameters

ParameterDefaultDescription
value-Current value, or null for empty
onValueChange-Called on button press or blur commit; receives null when the field is cleared
range0..10Accepted value bounds
step1Increment/decrement amount
suffix""Shown after the value inside the field, e.g. kg
placeholder"-"Text shown in the empty state when value is null
enabledtrueDisables interaction
statusnullValidation state; tints the inner text field border
flexiblefalseWhen true, fills maximum available width

Stepper.InputForm

Wraps Stepper.Input with a label, helper, and status message.

var weight by rememberSaveable { mutableStateOf<Int?>(70) }

Stepper.InputForm(
value = weight,
onValueChange = { weight = it },
label = "Weight",
helper = "Enter weight in kg",
range = 0..500,
step = 1,
required = true,
status = if (weight == null) FormFieldStatus.Error else null,
statusMessage = if (weight == null) "Required" else null,
)

State holder

val state = rememberStepperInputState(initialValue = 3)

Stepper.InputForm(
state = state,
label = "Quantity",
helper = "Enter a value between 0 and 100",
range = 0..100,
required = true,
)

Accessibility

stepperSemantics (Nudger)

Modifier.stepperSemantics configures a Nudger-containing layout to behave like a slider under TalkBack. Apply it when composing Stepper.Nudger inside a custom layout.

var value by rememberSaveable { mutableStateOf<Int?>(50) }
val label = "Volume"

Row(
modifier = Modifier
.fillMaxWidth()
.semantics { text = AnnotatedString(label) }
.stepperSemantics(
value = value,
onValueChange = { value = it },
range = 0..100,
step = 1,
suffix = "%",
enabled = true,
),
) {
Text(text = label, modifier = Modifier.invisibleSemantic())
Stepper.Nudger(
value = value,
onValueChange = { value = it },
range = 0..100,
allowSemantics = false,
)
}

The modifier exposes setProgress, overrides the percentage description, announces "disabled" when enabled = false, and handles Shift+Up/Down keyboard events.

Input variant

Stepper.Input exposes the editable text field as the single accessible control. TalkBack reads and edits the value through the field; the +/- buttons are visual only and hidden from accessibility, so they add no extra nodes to navigate.

Validation constraints

Both modifiers enforce:

  • step > 0

  • range.first % step == 0

  • range.last % step == 0

Migration from deprecated API

!WARNING Stepper() and StepperForm() are deprecated. Migrate to the new API below.

OldNew
Stepper(value, onValueChange, ...)Stepper.Nudger(value, onValueChange, ...)
StepperForm(value, onValueChange, label, helper, ...)Stepper.NudgerForm(value, onValueChange, label, helper, ...)

The deprecated functions still compile with a WARNING and internally delegate to the new API. Use the IDE quick-fix (ReplaceWith) to migrate call sites.

The status parameter of the old Stepper() is silently dropped; Stepper.Nudger has no status param. Use Stepper.NudgerForm if you need to show a validation status.

Types

Link copied to clipboard
object Stepper

Stepper variants for quantity picking with decrease and increase buttons.

Link copied to clipboard
Link copied to clipboard
class StepperInputState(val textFieldState: TextFieldState)

State holder for Stepper.Input and Stepper.InputForm.

Link copied to clipboard
class StepperState(initialValue: Int?, val range: IntRange, val step: Int)

State holder for Stepper.Nudger and Stepper.NudgerForm.

Functions

Link copied to clipboard

Creates and remembers a StepperInputState that survives recomposition and state restoration.

Link copied to clipboard
fun rememberStepperState(initialValue: Int? = null, range: IntRange = 0..10, step: Int = 1): StepperState

Creates and remembers a StepperState that survives recomposition and state restoration.

Link copied to clipboard
fun Stepper(value: Int, onValueChange: (Int) -> Unit, modifier: Modifier = Modifier, range: IntRange = 0..10, suffix: String = "", step: Int = 1, enabled: Boolean = true, status: FormFieldStatus? = null, flexible: Boolean = false, testTag: String? = null, allowSemantics: Boolean = true)
Link copied to clipboard
fun StepperForm(value: Int, onValueChange: (Int) -> Unit, label: String, helper: String?, modifier: Modifier = Modifier, range: IntRange = 0..10, suffix: String = "", step: Int = 1, enabled: Boolean = true, required: Boolean = false, status: FormFieldStatus? = null, statusMessage: String? = null, flexible: Boolean = false, testTag: String? = null)
Link copied to clipboard
fun Modifier.stepperSemantics(value: Int?, onValueChange: (Int) -> Unit, range: IntRange, step: Int, suffix: String?, enabled: Boolean): Modifier

Adds semantics to a Stepper component, enabling accessibility features from TalkBack.