UI containers
The structural elements: UIColumn stacks children vertically, UIRow horizontally,
UIScrollable makes an overflow region pan, UISpacer eats free space. For a whole screen
that scrolls, prefer UIScreen.makeScrollable() over wrapping everything in a UIScrollable —
see screen-router.md.
At a glance
const screen = UIScreen([
UIRow([
UIText("Library").style({ fontSize: 24, fontWeight: 700, color: "white" }),
UISpacer(), // pushes the count to the far edge
UIText("12 items").style({ color: "#888" }),
]).style({ px: 16, py: 12, alignItems: "center" }),
UIScrollable([
UIColumn([ UIText("First").style({ color: "white" }) ]).style({ p: 16 }),
UIColumn([ UIText("Second").style({ color: "white" }) ]).style({ p: 16 }),
]).style({ flexGrow: 1, flexShrink: 1, gap: 8 }) // flexShrink: 1 or it overflows
.onScroll(pos => console.log("scrolled to", pos)),
]).style({ bgColor: "black", pt: "safe-top" })
screen.open()UIRow / UIColumn
UIColumn(): UIColumn
UIColumn(children: UINodeChild[]): UIColumn
UIRow(children: UINodeChild[]): UIRow // no zero-arg overload — use UIRow([])The only difference between the two is the default flexDirection (column vs row) — both take
the full container style surface (UIContainerStyle = element + drawable + container layout
props, see styling.md). Children entries that are null / undefined / false
are skipped — see overview.md.
Avoid wrapper containers that only exist to align something — alignment is a container property:
UIRow([ UIColumn([]).style({ flexGrow: 1 }), label ]) // ✗ phantom spacer element
UIRow([ label ]).style({ justifyContent: "flex-end" }) // ✓Managing children
All containers on this page share the same imperative child API (works before and after the element is on screen; no diffing):
c.append(...nodes: UINodeChild[]): this
c.insert(index: number, ...nodes: UINodeChild[]): this // index into c.children
c.remove(...nodes: UINode[]): this // removes by identity
c.setContent(children: UINodeChild[]): this // replace everything
c.children // readonly UINodeChild[]setContent is the "re-render" primitive — build a fresh array (e.g. items.map(Row)) and swap
it in. For long or unbounded data use UIVirtualizedList instead.
UIBox — deprecated
UIBox(children | style | (style, children)): UIBox // legacy — use UIRow / UIColumnA legacy container whose only distinction is defaulting justifyContent and alignItems to
"center" (children centered on both axes). Kept for old projects; write new code with
UIRow/UIColumn plus explicit alignment.
UIScrollable
A pannable overflow region inside an otherwise fixed layout — a list under a pinned header, a
horizontal carousel. If the whole screen is one flow (article, feed), make the screen itself
scrollable instead: pull-to-refresh (onRefresh) exists only on UIScreen.makeScrollable().
UIScrollable(): UIScrollable
UIScrollable(children: UINodeChild[]): UIScrollableExtra styles on top of the container surface:
scrollDirection: "horizontal" | "vertical" // default vertical
showScrollbar: boolean
overscrollMode: "none" | "absorb" | "default" // edge behavior when dragged past the contentScroll events (chainable, like all on*):
s.onScroll(cb: (scrollPosition: number) => void): UIScrollable // logical px from the start edge
s.onScrollRelease(cb: () => void): UIScrollable // finger lifted
s.onOverscroll(cb: (delta: number) => void): UIScrollable // dragged past the edge (px)there is no scrollTo — a UIScrollable's position can't be set
programmatically. This is a current limitation. If you need programmatic scrolling
(scrollTo / scrollToEnd / scrollToKey), use UIVirtualizedList.
flexShrink defaults to 0 everywhere, so a vertical UIScrollable in a column
overflows the screen instead of scrolling until it — and any wrapping containers between it
and the screen — get flexShrink: 1.
Horizontal carousel:
UIScrollable(items.map(Card))
.style({ scrollDirection: "horizontal", showScrollbar: false, gap: 12, px: 16 })Draggable children inside a scroller need to claim their gesture direction or the scroll steals
the pointer — see touch.md.
UISpacer
UISpacer(): UISpacer // no children; its style surface is ElementStyle only — no backgroundFlexible empty space: it defaults to flexGrow: 1, eating free space along the parent's main
axis. Reach for it only when plain alignment can't express the layout — one item pushed to the
far end while the rest stay put:
UIRow([ title, UISpacer(), closeButton ])If all children move together, justifyContent ("space-between", "flex-end", …) does the
same job with no extra element.
See also
- UI element model — children arrays, conditional rendering, refs
- Styling — the container style vocabulary (
gap,alignItems, …) - Screens & navigation —
UIScreen.makeScrollable(),onRefresh - Virtualized lists — windowed rendering + programmatic scrolling