# 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.

```kotlin
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.

```kotlin
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.

## Related components

- [Time picker](https://m3e.ljzapps.app/docs/components/time-picker.md)
- [Slider](https://m3e.ljzapps.app/docs/components/slider.md)
- [Date picker](https://m3e.ljzapps.app/docs/components/date-picker.md)
