Scrolling and VirtualList
Scroll containers
Section titled “Scroll containers”An element scrolls when its classes say so: overflow-auto or
overflow-scroll, or per axis overflow-x-* and overflow-y-*. Give it a
size (h-96, flex-1 in a column, h-screen), as with CSS.
func Inbox(rows tela.Node) tela.Node { return Div(Class("h-96 overflow-y-auto rounded-lg border"), rows)}A scroll container scrolls with the wheel, the touchpad (with a kinetic fling
when the fingers lift), touch, the keyboard (arrows, Page Up and Down, Home and
End, Space) and its scrollbars. overflow-hidden scrolls only from code.
Wheel scrolling chains to the scrolling ancestors at an edge unless the element
has overscroll-contain.
The window scrolls like a browser page when the content is taller or wider
than it. h-screen and h-full on the outermost element still mean the
window’s height. Set RootClass: "overflow-hidden" on the tela.App to keep the
window from scrolling and scroll an inner element instead.
Scrollbars and animation
Section titled “Scrollbars and animation”By default scrollbars are thin overlay bars that appear while scrolling.
tela.App has two switches, and each has a class for a single element:
tela.App field |
Class | Effect |
|---|---|---|
ClassicScrollbars: true |
scrollbar-classic / scrollbar-overlay |
Always-visible classic bars that take space (a Windows look) |
InstantScroll: true |
scroll-instant / scroll-smooth |
Scrolling jumps instead of animating, and touch and touchpad scrolling do not fling |
The system’s reduced-motion setting always turns the animation off.
Fixed and sticky boxes
Section titled “Fixed and sticky boxes”A fixed box is placed against the window and does not move when the page
or a container scrolls; a sticky box scrolls with its container until its
insets stop it at an edge of the scrollport, and its parent’s end pushes it
out (Styling). Both follow scrolling without any work
but painting: a scroll frame records nothing again and moves them in the
compositor.
Scrolling from code
Section titled “Scrolling from code”Attach an tela.ElementRef with Ref (from tela.UseElementRef) and call its
methods from handlers and effects:
ElementRef.ScrollTo(x, y) andElementRef.ScrollBy(dx, dy) scroll to or by offsets, clamped to the content;ElementRef.ScrollOffsetreturns the offsets (the DOM’sscrollLeftandscrollTop). They jump, or animate when the element hasscroll-smooth.ElementRef.ScrollIntoViewscrolls every scrolling ancestor as little as possible so the element is visible.ElementRef.Boundsreturns the element’s border box in window coordinates as atela.Rect, moved by scrolling and transforms (getBoundingClientRect).
func Page(rows tela.Node) tela.Node { list := tela.UseElementRef() return Div(Class("relative"), Div(Ref(list), Class("h-96 overflow-y-auto scroll-smooth"), rows), Button(OnClick(func(tela.MouseEvent) { list.ScrollTo(0, 0) }), Text("Top")), )}Observing scrolling
Section titled “Observing scrolling”OnScroll runs after the element’s offsets changed, by the user or by code,
at most once per frame, on the scrolling element only (it does not bubble). The
tela.ScrollEvent has the DOM’s properties in logical pixels: ScrollLeft,
ScrollTop, ScrollWidth, ScrollHeight, ClientWidth and ClientHeight.
func Feed(rows tela.Node, loadMore func()) tela.Node { return Div(Class("h-96 overflow-y-auto"), OnScroll(func(e tela.ScrollEvent) { if e.ScrollTop+e.ClientHeight >= e.ScrollHeight-200 { loadMore() } }), rows, )}To tie a style to the scroll position without rebuilding anything, use
tela.UseScroll, which gives Values of the offsets that a deferred style
closure can read; see Hooks. OnWheel
observes the wheel and can cancel its scrolling; see Events.
VirtualList
Section titled “VirtualList”VirtualList is a vertical scrolling list of count items that builds, lays
out and paints only the items in view plus a few more on each side, so a list
of 100,000 rows costs about as much as one screenful of them. The node is
the scrolling element: props style it like a Div (Class, Style, Ref,
handlers, Key, Role and the Aria props), and it is overflow-y-auto
unless a class sets another overflow.
type Person struct{ Name string }
var people = make([]Person, 100_000)
type Row struct{ Index int }
func (r Row) Build() tela.Node { return Div(Class("flex h-16 items-center border-b px-4"), Textf("%d. %s", r.Index+1, people[r.Index].Name))}
func People() tela.Node { return VirtualList(len(people), func(int) float32 { return 64 }, // estimated height of item i func(i int) tela.Component { return Row{Index: i} }, Class("h-96 rounded-lg border"), )}estimateSize(i)estimates the height of itemibefore it is built; the item is then measured once laid out, and the measurement replaces the estimate. Items may have any height, and the scrollbar reflects the whole list. When a measurement changes an item above the viewport, the offset moves by the difference in the same frame, so the content on screen does not jump.renderItem(i)returns the item as atela.Nodeor atela.Component(a struct component such asRow{Index: i}). It runs inside the list’s build, so it must not call hooks: return a component (a struct component, or a node fromtela.Cortela.CP) for items with state. A struct component skips its build when its fields did not change, so a list rebuilt by its parent rebuilds only the changed items.- Items are keyed by index unless the list has
ItemKey(func(i int) K), which keys them by the item’s identity, so an item component’s state follows the item when others are inserted or removed before it:ItemKey(func(i int) int { return rows[i].ID }). The focused item stays mounted while it is scrolled away, so Tab goes on from it. ItemHeight(px)gives every item the same height: nothing is measured, positions are exact from the first frame, andestimateSizemay benil. An item taller than that spills over the next one.- The list has the role
listand its itemslistitem(in the aria snapshot and for assistive technology);Roleon the list replaces its role, and the items are then no listitems. An item’s own role goes on the elementrenderItemreturns. - Scrolling inside the built items only repaints; scrolling past them builds the entering items and unmounts the leaving ones.
Overscansets how many items are built beyond each side of the visible ones (5 by default). More items cost more layout and paint per frame; 0 rebuilds the list on every scroll that shows a new item.- Lists are vertical; horizontal lists and grids are not virtualized yet.
- Items can be
sticky: a section header withsticky top-0sticks while the items after it scroll by, and the next header covers it (the item wrappers do not contain it; the list does). The last sticky item above the window stays mounted while it may be stuck. A header the list never built (the list jumped past it withScrollToIndex) is not known to be sticky until it is built.
Scrolling to an item
Section titled “Scrolling to an item”ElementRef.ScrollToIndex(i, align) scrolls to item i and keeps it
aligned while the items around it are measured. The alignments are
tela.AlignAuto (as little as possible), tela.AlignStart, tela.AlignCenter
and tela.AlignEnd (TanStack Virtual’s align). It jumps, or animates with
scroll-smooth.
func Jump() tela.Node { list := tela.UseElementRef() return Div(Class("flex h-screen flex-col"), Button(OnClick(func(tela.MouseEvent) { list.ScrollToIndex(5000, tela.AlignCenter) }), Text("Go to 5000")), VirtualList(100_000, func(int) float32 { return 40 }, func(i int) tela.Node { return Div(Class("h-10"), Textf("Row %d", i)) }, Ref(list), Class("flex-1 rounded-lg border")), )}The examples/virtuallist screen shows 100,000 people with variable row heights
and ScrollToIndex. On the CPU rasterizer, scrolling it has a p95 of 2.3 ms per
frame.
Testing scrolling
Section titled “Testing scrolling”App.Scroll scrolls the wheel over an element like a touchpad, and the
auto-wait scrolls an element into view before clicking it; see
Testing.