---
title: Scroll
url: "https://native-sdk.dev/docs/components/scroll"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `scroll` for a viewport whose content exceeds its bounds. Wrap multiple children in one column. The engine handles wheel, kinetic, and keyboard scrolling and displays a scrollbar during scrolling. `on-scroll` delivers offsets, velocities, and viewport and content extents for both axes. In TypeScript, import `ScrollState` from `@native-sdk/core/events` and use camelCase fields such as `offsetY`. Zig uses `canvas.ScrollState` with snake\_case fields such as `offset_y`.

Set `axis` to `vertical` (the default), `horizontal`, or `both`. Horizontal regions handle horizontal wheel input, display a bottom scrollbar, and support Left/Right keys. Horizontal-only regions also use Home/End/PageUp/PageDown on that axis. Bind `value` for the vertical offset and `value-x` for the horizontal offset to control position from the model.

Nested regions route each wheel axis to the nearest ancestor that supports it. For example, a vertical list inside a horizontal timeline handles vertical input in the list and horizontal input in the timeline. Virtualized scroll regions support only the vertical axis.

Scrolling stops at content edges by default. Set `overscroll="rubber_band"` to allow a region to bounce past its edges. `ScrollPhysics.overscroll` sets the app-wide default; a region’s value overrides it.

`on-reach-end` dispatches a message when the offset comes within one viewport of the content end. It fires once per approach. Adding content can re-arm it; otherwise re-arming requires a post-scroll observation at least 1.5 viewports from the end. A programmatic jump to the end fires once. Use this event to request the next batch of content. Pair it with [list](/docs/components/list) to limit layout and painting, or the Zig builder’s [virtual list](/docs/components/virtual-list) to build only the visible rows.

## Markup

```html
<scroll height="240" padding="8" on-scroll="log_scrolled">
  <column gap="2">
    <list-item on-press="open_entry">Changelog entry 14</list-item>
    <list-item on-press="open_entry">Changelog entry 13</list-item>
    <list-item on-press="open_entry">Changelog entry 12</list-item>
    <list-item on-press="open_entry">Changelog entry 11</list-item>
    <list-item on-press="open_entry">Changelog entry 10</list-item>
    <list-item on-press="open_entry">Changelog entry 9</list-item>
    <list-item on-press="open_entry">Changelog entry 8</list-item>
  </column>
</scroll>
```

The arm receives the scroll state and stores what the model wants to remember — echo `offsetY` into a field bound as the scroll's `value` (and `offsetX` into `value-x` on horizontal-capable regions) and the model owns the position too:

```ts
import { type ScrollState } from "@native-sdk/core/events";

export type Msg = /* ... */ | { readonly kind: "log_scrolled"; readonly scroll: ScrollState };
// in update:
case "log_scrolled":
  return { ...model, changelog_offset: msg.scroll.offsetY };
```

```zig
// Msg arm: log_scrolled: canvas.ScrollState
.log_scrolled => |scroll| model.changelog_offset = scroll.offset_y,
```

A horizontal shelf declares its axis and reads the `X` fields:

```html
<scroll axis="horizontal" height="180" on-scroll="shelf_scrolled">
  <row gap="8">
    <panel width="140"><text>Cover 1</text></panel>
    <panel width="140"><text>Cover 2</text></panel>
    <panel width="140"><text>Cover 3</text></panel>
  </row>
</scroll>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically. `on_scroll` pairs with `Ui.scrollMsg(.tag)`; the delivered offset is the value the runtime already applied, so echoing it back never fights the scroll reconcile.

```zig
ui.scroll(.{ .height = 240, .padding = 8, .on_scroll = Ui.scrollMsg(.log_scrolled) }, .{
    ui.column(.{ .gap = 2 }, .{
        ui.listItem(.{ .on_press = .open_entry }, "Changelog entry 14"),
        ui.listItem(.{ .on_press = .open_entry }, "Changelog entry 13"),
        ui.listItem(.{ .on_press = .open_entry }, "Changelog entry 12"),
        ui.listItem(.{ .on_press = .open_entry }, "Changelog entry 11"),
        ui.listItem(.{ .on_press = .open_entry }, "Changelog entry 10"),
    }),
})
```

## Attributes

| Attribute      | Description                                                                                                                                                                                                                                                                                                                                                                                        |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `on-scroll`    | scroll element only: names a Msg variant with canvas.ScrollState payload; delivers the post-scroll two-axis state (offset\_x/offset\_y, velocity\_x/velocity\_y, viewport\_extent\_x/viewport\_extent\_y, content\_extent\_x/content\_extent\_y) after wheel, kinetic, keyboard, and accessibility scrolls.                                                                                        |
| `on-reach-end` | scroll element only: Msg (tag or tag:\{payload}) dispatched when a user scroll comes within one viewport of the content end - the infinite-scroll fetch signal. Fires once per approach with hysteresis: it re-arms only after the offset retreats past 1.5 viewports, which appending a batch causes on its own by growing the extent.                                                            |
| `axis`         | scroll only: which axes the region scrolls - vertical (the default), horizontal, or both. Horizontal grants opt the region into wheel/trackpad delta-x, the bottom-edge scrollbar, and the horizontal keymap; in a nested tree each wheel axis routes independently to the nearest ancestor scrolling that axis. Virtualized scrolls stay vertical (a horizontal grant there is a teaching error). |
| `value-x`      | scroll only, beside axis="horizontal" or axis="both": the horizontal scroll offset - the sideways counterpart of value, following the same source-wins reconcile rule (echo on-scroll's offset\_x back to keep user scrolling; move it model-side to scroll programmatically). Without a horizontal axis grant it is a teaching error (it would be silently inert).                                |
| `overscroll`   | scroll only: edge behavior of the region. none pins scrolling at the content edges (the shipped default via the ScrollPhysics.overscroll token), rubber\_band lets this region bounce past them, default follows the token. Honored by the engine's scroll physics and the native OS scroller alike.                                                                                               |
| `height`       | Definite height (plain number): the element is exactly this tall; content neither shrinks nor overflows it.                                                                                                                                                                                                                                                                                        |
| `width`        | Definite width (plain number): the element is exactly this wide; content neither shrinks nor overflows it. On resizable it is the initial width.                                                                                                                                                                                                                                                   |
| `padding`      | Uniform padding (plain number).                                                                                                                                                                                                                                                                                                                                                                    |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)