Scroll
A scroll view: wrap multiple children in a single column inside it. The engine owns wheel, kinetic, and keyboard scrolling and draws the scrollbar while a scroll is in flight; on-scroll names a Msg variant with a canvas.ScrollState payload that delivers the post-scroll offset and viewport/content extents, so the model can observe position without owning it. Scrolling pins at the content edges by default — no rubber-band bounce; kinetic motion stops cleanly at the boundary. overscroll="rubber_band" opts one region into bouncing past its edges (both the engine physics and the native macOS scroller honor it), and the ScrollPhysics.overscroll design token flips the app-wide default, which per-region values override. on-reach-end dispatches a plain Msg when a scroll comes within one viewport of the content end — the infinite-fetch signal, fired once per approach with hysteresis (appending a batch grows the extent and re-arms the next approach). A programmatic jump to the end fires once and never re-arms while the offset stays near the end — re-arming needs a post-scroll observation at least 1.5 viewports from it. Pair with list for layout-culled rows, or the builder's virtual list for dataset-scale windows.

Markup
<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>Zig builder
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.
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 offset/viewport/content extents 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. |
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). |
