Scroll

Scroll modifiers

The verticalScroll and horizontalScroll modifiers provide the simplest way to allow the user to scroll an element when the bounds of its contents are larger than its maximum size constraints. With the verticalScroll and horizontalScroll modifiers you don't need to translate or offset the contents.

@Composable
private fun ScrollBoxes() {
    Column(
        modifier = Modifier
            .background(Color.LightGray)
            .size(100.dp)
            .verticalScroll(rememberScrollState())
    ) {
        repeat(10) {
            Text("Item $it", modifier = Modifier.padding(2.dp))
        }
    }
}

A simple vertical list responding to scroll
gestures
Figure 1. A simple vertical list responding to scroll gestures.

The ScrollState lets you change the scroll position or get its current state. To create it with default parameters, use rememberScrollState().

@Composable
private fun ScrollBoxesSmooth() {
    // Smoothly scroll 100px on first composition
    val state = rememberScrollState()
    LaunchedEffect(Unit) { state.animateScrollTo(100) }

    Column(
        modifier = Modifier
            .background(Color.LightGray)
            .size(100.dp)
            .padding(horizontal = 8.dp)
            .verticalScroll(state)
    ) {
        repeat(10) {
            Text("Item $it", modifier = Modifier.padding(2.dp))
        }
    }
}

Scrollable area modifier

The scrollableArea modifier is a fundamental building block for creating custom scrollable containers. It provides a higher-level abstraction over the scrollable modifier, handling common requirements like gesture delta interpretation, content clipping, and overscroll effects.

While scrollableArea is used for custom implementations, you should generally prefer ready-made solutions like verticalScroll, horizontalScroll, or composables like LazyColumn for standard scrolling lists. These higher-level components are simpler for common use cases and are themselves built by using scrollableArea.

Difference between scrollableArea and scrollable modifiers

The main difference between scrollableArea and scrollable lies in how they interpret user scroll gestures:

  • scrollable (raw delta): The delta directly reflects the physical movement of the user's input (e.g., pointer drag) on the screen.
  • scrollableArea (content-oriented delta): The delta is semantically inverted to represent the selected change in the scroll position to make the content appear to move with the user's gesture, which is usually the opposite of the pointer movement.

Think of it like this: scrollable tells you how the pointer moved, while scrollableArea translates that pointer movement into how the content should move within a typical scrollable view. This inversion is why scrollableArea feels more natural when implementing a standard scrollable container.

The following table summarizes the delta signs for common scenarios:

User gesture

delta reported to dispatchRawDelta by scrollable

delta reported to dispatchRawDelta by scrollableArea*

Pointer moves UP

Negative

Positive

Pointer moves DOWN

Positive

Negative

Pointer moves LEFT

Negative

Positive (Negative for RTL)

Pointer moves RIGHT

Positive

Negative (Positive for RTL)

(*) Note on scrollableArea delta sign: The sign of the delta from scrollableArea is not just a simple inversion. It intelligently considers:

  1. Orientation: Vertical or horizontal.
  2. LayoutDirection: LTR or RTL (especially important for horizontal scrolling).
  3. reverseScrolling flag: Whether scroll direction is inverted.

In addition to inverting the scroll delta, scrollableArea also clips the content to the bounds of the layout and handles the rendering of overscroll effects. By default, it uses the effect provided by LocalOverscrollFactory. You can customize or disable this by using the scrollableArea overload that accepts an OverscrollEffect parameter.

When to use scrollableArea modifier

You should use the scrollableArea modifier when you need to build a custom scrolling component that isn't adequately served by the horizontalScroll or verticalScroll modifiers or Lazy layouts. This often involves cases with:

  • Custom layout logic: When the arrangement of items changes dynamically based on the scroll position.
  • Unique visual effects: Applying transformations, scaling, or other effects to children as they scroll.
  • Direct control: Needing fine-grained control over the scrolling mechanics beyond what verticalScroll or Lazy layouts expose.

Create custom wheel-like lists using scrollableArea

The following sample demonstrates using scrollableArea to build a custom vertical list where items scale down as they move away from the center, creating a "wheel-like" visual effect. This kind of scroll-dependent transformation is a perfect use case for scrollableArea.

Figure 2. A customized vertical list using scrollableArea.

@Composable
private fun ScrollableAreaSample() {
    // ...
    Layout(
        modifier =
            Modifier
                .size(150.dp)
                .scrollableArea(scrollState, Orientation.Vertical)
                .background(Color.LightGray),
        // ...
    ) { measurables, constraints ->
        // ...
        // Update the maximum scroll value to not scroll beyond limits and stop when scroll
        // reaches the end.
        scrollState.maxValue = (totalHeight - viewportHeight).coerceAtLeast(0)

        // Position the children within the layout.
        layout(constraints.maxWidth, viewportHeight) {
            // The current vertical scroll position, in pixels.
            val scrollY = scrollState.value
            val viewportCenterY = scrollY + viewportHeight / 2

            var placeableLayoutPositionY = 0
            placeables.forEach { placeable ->
                // This sample applies a scaling effect to items based on their distance
                // from the center, creating a wheel-like effect.
                // ...
                // Place the item horizontally centered with a layer transformation for
                // scaling to achieve wheel-like effect.
                placeable.placeRelativeWithLayer(
                    x = constraints.maxWidth / 2 - placeable.width / 2,
                    // Offset y by the scroll position to make placeable visible in the viewport.
                    y = placeableLayoutPositionY - scrollY,
                ) {
                    scaleX = scaleFactor
                    scaleY = scaleFactor
                }
                // Move to the next item's vertical position.
                placeableLayoutPositionY += placeable.height
            }
        }
    }
}
// ...

Scrollable modifier

The scrollable modifier differs from the scroll modifiers in that scrollable detects the scroll gestures and captures the deltas, but does not offset its contents automatically. This is instead delegated to the user through ScrollableState , which is required for this modifier to work correctly.

When constructing ScrollableState you must provide a consumeScrollDelta function which will be invoked on each scroll step (by gesture input, smooth scrolling or flinging) with the delta in pixels. This function must return the amount of scrolling distance consumed, to ensure the event is properly propagated in cases where there are nested elements that have the scrollable modifier.

The following snippet detects the gestures and displays a numerical value for an offset, but does not offset any elements:

@Composable
private fun ScrollableSample() {
    // actual composable state
    var offset by remember { mutableFloatStateOf(0f) }
    Box(
        Modifier
            .size(150.dp)
            .scrollable(
                orientation = Orientation.Vertical,
                // Scrollable state: describes how to consume
                // scrolling delta and update offset
                state = rememberScrollableState { delta ->
                    offset += delta
                    delta
                }
            )
            .background(Color.LightGray),
        contentAlignment = Alignment.Center
    ) {
        Text(offset.toString())
    }
}

A UI element detecting the finger press and displaying the numeric value for the finger's location
Figure 3. A UI element detecting the finger press and displaying the numeric value for the finger's location.