Skip to content

Layout

Every widget gets a rectangular region (x, y, width, height in terminal cells) computed once per frame from its style and its parent’s. There are no floats or text reflow — just nested boxes, sized and stacked. If you know CSS flexbox, most of this will feel familiar; the vocabulary is deliberately a subset.

A container lays its children out in one direction. Pick it with layout (or the React shorthand components <VBox> / <HBox>):

import { VBox, HBox, Label } from "@huyz0/ztui/react";
<VBox> {/* layout: "vertical" — top to bottom (the default) */}
<Label>top</Label>
<Label>bottom</Label>
</VBox>
<HBox> {/* layout: "horizontal" — left to right */}
<Label>left</Label>
<Label>right</Label>
</HBox>

<VBox> and <HBox> are just <View> with layout preset; you can always set style={{ layout: "vertical" | "horizontal" }} yourself. (display: "flex" with flexDirection: "row" | "column" is accepted as an alias.)

width and height each accept four forms:

FormExampleMeaning
cellswidth: 20a fixed number of terminal cells
percentwidth: "50%"a fraction of the parent’s content box
fractionalheight: "1fr"a share of the leftover space after fixed/auto
autowidth: "auto"shrink to fit the content (the default)

Fractional units split whatever space remains along the flow axis. Two siblings at "1fr" and "2fr" take one-third and two-thirds of the leftover:

<HBox style={{ height: 10 }}>
<VBox style={{ width: "1fr", background: "$surface" }} />
<VBox style={{ width: "2fr", background: "$panel" }} />
</HBox>

flexGrow: n is equivalent to n fr on the flow axis. Clamp any of these with minWidth / maxWidth / minHeight / maxHeight.

Auto-sized content is always clamped to the space the parent offers — a long label is truncated to the available width rather than overflowing.

padding insets the content from the box edge; margin adds space outside it. Both take a number (all sides), a { top, right, bottom, left } object, or a Spacing instance:

<VBox style={{ padding: 1, margin: { top: 1, bottom: 1 }, border: "rounded" }}>
</VBox>

A border draws a one-cell frame on all four sides and shrinks the content box accordingly. Color it with borderColor. The weight is the box-drawing style: "rounded" (default), "thin", "solid", "heavy", "double", "dashed", "bar" (half-block accent ), "block" (full-cell solid ), or "none".

borderTop, borderRight, borderBottom, and borderLeft set a single edge (same weight values) and override border for that side. A lone side is a clean corner-less bar that only insets layout on that side — handy for a chat bubble whose colored bar says who a message is from and how important it is, the same way Toast uses color for info/warn/error:

// user: a thick teal bar; assistant: a thin muted bar; alert: a solid red bar
<VBox style={{ borderLeft: "heavy", borderColor: "$primary", padding: { left: 1 } }}></VBox>
<VBox style={{ borderLeft: "thin", borderColor: "$dimmed", padding: { left: 1 } }}></VBox>
<VBox style={{ borderLeft: "bar", borderColor: "$error", padding: { left: 1 } }}></VBox>

Set a side to "none" to drop just that edge of an all-sides border.

Within a container, align children on the cross axis:

  • align: "left" | "center" | "right" — horizontal placement.
  • verticalAlign: "top" | "middle" | "bottom" — vertical placement.
<VBox style={{ height: 10, align: "center", verticalAlign: "middle" }}>
<Label>centered both ways</Label>
</VBox>

<Dock> (or layout: "dock") pins children to an edge and lets the rest fill what’s left — the classic header/footer/body shell. Each child opts into an edge with dock; an undocked child fills the remaining center:

import { Dock, Header, Footer, VBox } from "@huyz0/ztui/react";
<Dock>
<Header>title bar</Header> {/* docks top */}
<VBox style={{ dock: "left", width: 24 }}>nav</VBox>
<VBox>main content fills the rest</VBox>
<Footer>status bar</Footer> {/* docks bottom */}
</Dock>

<Header> and <Footer> are pre-docked to the top and bottom edges.

<Grid> (display: "grid") tiles its children into equal cells — a quick way to lay out a uniform gallery without sizing each child. For non-uniform splits, prefer nested HBox/VBox with fr units, which give you full control.

Set position: "absolute" to take a widget out of flow and place it with left / top / right / bottom relative to its parent’s content box. Use zIndex to control stacking order among siblings. For modals, dropdowns, and floating panels, prefer the overlay components — they portal to a screen-level layer so they’re never clipped by a parent.

A container clips its children to its content box by default, so an oversized or mispositioned child can never paint over its own border or a sibling. Opt out per axis with overflow:

  • overflowX / overflowY: "visible" — let content spill (rarely needed).
  • "hidden" — clip (the default behavior).
  • "scroll" / "auto" — clip and make the box scrollable. <ScrollableBox> presets this; scrolling responds to the wheel and PageUp/PageDown.

Each frame the engine walks the tree twice: a measure pass computes each widget’s intrinsic size bottom-up (so auto and fr know their content), then a layout pass assigns regions top-down. You never call either directly — set styles and let the engine resolve them. See Architecture for where layout sits in the full pipeline.