Scroll field
A vertical wheel that snaps to one value, for times and other short cyclic ranges.
- Category: Selection and input
- API status: Experimental, needs
@OptIn(ExperimentalMaterial3ExpressiveApi::class) - Part of the Material 3 Expressive update
- Compose APIs:
ScrollField,rememberScrollFieldState,ScrollFieldState,ScrollFieldDefaults - Interactive page: https://m3e.ljzapps.app/#components/scroll-field
When to use
ScrollField shows a wheel of values that the user flings or drags. The value in the middle is the selected one. The default item renders the index as a number with two digits, which suits hours and minutes.
The wheel always wraps around: after the last item the first one follows. Use it for cyclic values such as time, and a slider or a text field for open ranges.
In practice
API details and pitfalls of this Material3 version that the design guidelines do not cover.
- **Item height is fixed.** Each item is a third of ScrollFieldDefaults.ScrollFieldHeight (200 dp) tall, whatever height you give the field. Use that height to see one item above and one below the selection. A smaller field only cuts the neighbors off.
- **No accessibility parameters in this version.** ScrollField in 1.12.0-alpha03 takes no contentDescription. Add Modifier.semantics with a contentDescription ("Hour") and a stateDescription with the current value, otherwise a screen reader only hears the numbers. Newer alphas add parameters for this.
- **It always wraps around.** The wheel is a pager with a very large page count that starts in the middle, so it never reaches an end. There is no switch for a bounded list.
- **Changing the count needs a new state.** rememberScrollFieldState(itemCount, index) is keyed on both values. If the count changes, for example days in a month, the wheel jumps to the initial index again. Keep the selected value yourself and pass it as index.
- **The default item pads with a zero.** ScrollFieldDefaults.Item shows 7 as 07. For values that are not indices, or that should not be padded, pass your own field lambda.
Playground
The interactive page has a playground with live property controls for this component and generated code.
Examples
Time
Two fields for hour and minute, with labels for screen readers.
val hours = rememberScrollFieldState(itemCount = 24, index = 7)
val minutes = rememberScrollFieldState(itemCount = 60, index = 30)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
ScrollField(
state = hours,
modifier = Modifier
.size(width = 112.dp, height = ScrollFieldDefaults.ScrollFieldHeight)
.semantics {
contentDescription = "Hour"
stateDescription = "${hours.selectedOption}"
},
)
Text(
":",
style = MaterialTheme.typography.displayLarge,
modifier = Modifier.semantics { hideFromAccessibility() },
)
ScrollField(
state = minutes,
modifier = Modifier
.size(width = 112.dp, height = ScrollFieldDefaults.ScrollFieldHeight)
.semantics {
contentDescription = "Minute"
stateDescription = "${minutes.selectedOption}"
},
)
}
Text("Alarm at ${hours.selectedOption}:${minutes.selectedOption.toString().padStart(2, '0')}")
}
Custom items
The field lambda renders anything, here units of volume.
val units = listOf("ml", "cl", "l", "oz", "cup")
val state = rememberScrollFieldState(itemCount = units.size, index = 2)
val colors = ScrollFieldDefaults.colors()
ScrollField(
state = state,
modifier = Modifier
.size(width = 140.dp, height = ScrollFieldDefaults.ScrollFieldHeight)
.semantics {
contentDescription = "Unit"
stateDescription = units[state.selectedOption]
},
colors = colors,
// Any content works. The field gets the index and whether it is the selected one.
field = { index, selected ->
Text(
units[index],
style = if (selected) MaterialTheme.typography.headlineLargeEmphasized else MaterialTheme.typography.headlineSmall,
color = if (selected) colors.selectedContentColor else colors.unselectedContentColor,
)
},
)
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
state | ScrollFieldState | rememberScrollFieldState(itemCount, index). selectedOption reads the selected index. | |
modifier | Modifier | Modifier | Size the field here. The default item needs about 112 dp of width. |
colors | ScrollFieldColors | ScrollFieldDefaults.colors() | containerColor, unselectedContentColor and selectedContentColor. |
field | @Composable (index: Int, selected: Boolean) -> Unit | ScrollFieldDefaults.Item | Content of one item. |
scrollToOption / animateScrollToOption | suspend (Int) -> Unit | ScrollFieldState. Jump or animate to an index from code. |
Guidelines
A summary. The full design guidelines are at https://m3.material.io/components.
Do:
- Use it for short, cyclic ranges such as hours, minutes or weekdays.
- Pair fields that belong together in one row.
Don't:
- Do not use it for long, open ranges.
- Do not shrink it so the neighbors of the selected value disappear.
Accessibility
- Give every field a contentDescription and a stateDescription with the selected value through Modifier.semantics.
- Hide decorative separators such as the colon between hour and minute from accessibility services.
- Offer a text input as an alternative for long ranges, where scrolling to a value takes many flings.