Rezi uses a cell-based layout system: all sizes and coordinates are measured in terminal character cells (not pixels). This makes layout deterministic across platforms and terminal emulators.
This page covers the core layout primitives and the props that control sizing and positioning.
Every widget is laid out into a rectangle:
x,y— top-left corner in cells (0,0 is the top-left of the terminal)w,h— width/height in cells
(0,0) ───────────────────────────► x
│
│ ┌────────────── w ──────────────┐
│ │ │
▼ │ h │
y │ │
└────────────────────────────────┘
The primary layout containers are stacks:
ui.row(props, children)— horizontal stackingui.column(props, children)— vertical stacking
Key props:
gap— spacing between children (cells or spacing key)align— cross-axis alignment:"start" | "center" | "end" | "stretch"justify— main-axis distribution:"start" | "end" | "center" | "between" | "around" | "evenly"
import { ui } from "@rezi-ui/core";
import { createNodeApp } from "@rezi-ui/node";
const app = createNodeApp({
initialState: {},
});
app.view(() =>
ui.column({ p: 1, gap: 1 }, [
ui.text("Header"),
ui.row({ gap: 2, justify: "between" }, [
ui.text("Left"),
ui.text("Right"),
]),
])
);
await app.start();Container widgets accept spacing props (values are cells, or named keys like "sm", "md", "lg"):
p(all),px/py,pt/pr/pb/pl- Must be non-negative (
>= 0) when provided as numbers.
m(all),mx/my,mt/mr/mb/ml- May be negative (signed int32), which allows intentional overlap.
Example:
import { ui } from "@rezi-ui/core";
ui.box({ p: "md", mx: "lg", border: "rounded", title: "Panel" }, [
ui.text("Content"),
]);Notes:
- Padding reduces the available content area for children.
- Margin affects how the widget is positioned inside its parent stack.
- Negative margins can move a child outside the parent's origin and can cause overlap.
Example 1: overlap siblings in a row
import { ui } from "@rezi-ui/core";
ui.row({}, [
ui.box({ border: "none", width: 12, p: 1 }, [ui.text("Base")]),
ui.box({ border: "rounded", width: 10, ml: -6, p: 1 }, [ui.text("Overlay")]),
]);Example 2: pull content upward in a column
import { ui } from "@rezi-ui/core";
ui.column({ gap: 1 }, [
ui.box({ border: "single", p: 1 }, [ui.text("Header block")]),
ui.box({ border: "rounded", mt: -1, p: 1 }, [ui.text("Raised panel")]),
]);Rules summary:
m/mx/my/mt/mr/mb/mlaccept signed int32 numbers (and spacing keys).p/px/py/pt/pr/pb/pl, legacypad, andgapmust stay non-negative.- Computed
w/hare always clamped to non-negative values. - Computed
x/ycan be negative when margins pull widgets outward.
Alignment depends on the stack direction:
- In a
row,aligncontrols vertical alignment;justifycontrols horizontal distribution. - In a
column,aligncontrols horizontal alignment;justifycontrols vertical distribution.
Example:
import { ui } from "@rezi-ui/core";
ui.row({ height: 3, align: "center", justify: "between" }, [
ui.text("A"),
ui.text("B"),
]);Most container widgets accept layout constraints:
width/height: number of cells, percentage string ("50%"), or"auto"minWidth/maxWidth,minHeight/maxHeightflex: main-axis space distribution insiderow/columnaspectRatio: enforcew/h
Example: fixed + flex children in a row
import { ui } from "@rezi-ui/core";
ui.row({ gap: 1 }, [
ui.box({ width: 20, border: "single" }, [ui.text("Fixed 20")]),
ui.box({ flex: 1, border: "single" }, [ui.text("Flex fill")]),
]);ui.box can draw a border around its content:
border:"none" | "single" | "double" | "rounded" | "heavy" | "dashed" | "heavy-dashed"title: optional title rendered in the top bordertitleAlign:"left" | "center" | "right"
Border thickness is 1 cell on each edge (unless border: "none"). Padding is applied inside the border.
Example:
import { ui } from "@rezi-ui/core";
ui.box({ title: "Settings", titleAlign: "center", border: "double", p: 1 }, [
ui.text("Option A"),
ui.text("Option B"),
]);Nesting is just composition: put stacks/boxes inside stacks/boxes.
Example: sidebar + content column
import { ui } from "@rezi-ui/core";
ui.row({ gap: 1 }, [
ui.box({ width: 24, border: "rounded", p: 1, title: "Sidebar" }, [
ui.column({ gap: 1 }, [ui.text("One"), ui.text("Two")]),
]),
ui.box({ flex: 1, border: "rounded", p: 1, title: "Content" }, [
ui.text("Main area"),
]),
]);Rezi clips rendering to each widget’s allocated rect. Overflow is handled per-widget:
ui.textsupportstextOverflow: "clip" | "ellipsis" | "middle"(andmaxWidth)- Containers clip their children to the padded/bordered content area
Example: ellipsis truncation
import { ui } from "@rezi-ui/core";
ui.box({ width: 20, border: "single", p: 1 }, [
ui.text("This is a long line that will truncate", { textOverflow: "ellipsis" }),
]);When widgets overlap, input routing is deterministic:
- Layers: higher
zIndexwins. - Layers with equal
zIndex: later registration wins. - Regular layout tree (no layer distinction): the last focusable widget in depth-first preorder tree order wins.
- This means later siblings win ties.
- Negative margins can make
x/ynegative; this is expected and supported. - Large negative margins can significantly increase overlap. Keep fixtures for critical layouts.
padandgapdo not allow negatives; use margins when you need pull/overlap effects.- In overlap regions, tie-breaks follow deterministic order, not visual styling alone.
- Concepts - How VNodes and reconciliation work
- Lifecycle & Updates - When layout runs and why updates are committed
- Styling - Background fills, borders, and style inheritance
Next: Input & focus.