4.3k

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.

scroll
A scroll region rendered by the engine (light theme)
a fixed-height scroll region; the engine draws the scrollbar during scrolling

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

AttributeDescription
on-scrollscroll 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-endscroll 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.
overscrollscroll 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.
heightDefinite height (plain number): the element is exactly this tall; content neither shrinks nor overflows it.
widthDefinite width (plain number): the element is exactly this wide; content neither shrinks nor overflows it. On resizable it is the initial width.
paddingUniform padding (plain number).