Skip to content

Styling

Widgets are styled with a single style prop: a plain object of properties, inline like React’s style but typed to what a terminal cell can actually show. There are no stylesheets to wire up for the common case — set properties and they apply.

import { Label } from "@huyz0/ztui/react";
<Label style={{ color: "$primary", bold: true, background: "$surface" }}>
Hello
</Label>;

color (foreground) and background accept:

  • Theme tokens"$primary", "$surface", "$error", … Resolve against the active theme, so they adapt when the theme changes. Prefer these — see Theming for the full token list.
  • Hex"#4daafc" (truecolor; degraded automatically on 256-color terminals).
  • Named colors — standard CSS/ANSI names like "red", "cyan", "gray".
  • "transparent" — let the layer below show through.
<VBox style={{ background: "$panel" }}>
<Label style={{ color: "$success" }}>ok</Label>
<Label style={{ color: "#e06c75" }}>literal hex</Label>
</VBox>

Any string value may embed $name (or the CSS form var(--name)); it’s replaced with the active theme’s color before painting. This is the portable way to color things — the same widget tree looks right under every theme and on both backends. Tokens resolve per-widget, so a widget’s theme override flows to its descendants.

Booleans toggle terminal cell attributes:

PropertyEffect
boldbold / bright text
italicitalic (where the terminal supports it)
underlineunderline
strikethroughstruck-through
dimreduced intensity
reverseswap fg/bg
<Label style={{ bold: true, underline: true }}>emphasis</Label>

link: "https://…" marks the text as a hyperlink (OSC 8) on terminals that support it.

cursor sets the mouse-pointer shape shown while the pointer is over a widget, using the CSS cursor names (pointer, text, grab, grabbing, not-allowed, progress, wait, crosshair, move, zoom-in, and the *-resize family — note the OSC 22 names like ew-resize/ns-resize, not col-resize):

<Box style={{ cursor: "pointer" }} onClick={}>Open</Box>
<Box style={{ cursor: "text" }}>Editable</Box>
<Box style={{ cursor: "ew-resize" }}>│ drag to resize</Box>

The shape is inherited from the nearest ancestor that sets cursor, so wrapping a clickable region is enough — children don’t each need it. It relies on the terminal’s OSC 22 support (capabilities.pointerShapes: kitty, foot, recent xterm; opt-in on Alacritty) and is ignored elsewhere, with the pointer reset to the default arrow over empty space and on exit.

Built-in widgets already pick a sensible default, so you usually don’t set cursor yourself: interactive controls (buttons, checkboxes, switches, radios, toggle buttons, selects, sliders, menus, tabs, collapsibles, lists/trees) and any widget with an onClick show pointer; text inputs, textareas, and the chat composer show text; and a disabled control shows not-allowed. Set cursor explicitly to override.

Sizing, spacing, borders, alignment, docking, and overflow are all set through the same style object and are covered in Layout: width/height, padding/margin, border/borderColor, align, dock, overflowX/overflowY, and so on.

Every widget has a defaultStyle (its built-in look) that your style props override key-by-key. So you only specify what differs:

<Button style={{ background: "$success" }}>Save</Button> // keeps the rest of Button's look

Interactive widgets (Button, Input, Select, …) manage their own focus and hover visuals — a focused control gets a gently animated focus ring, hovered controls highlight — so you don’t style those states by hand. For custom :hover / :focus rules on your own widgets you can load a CSS-like stylesheet with pseudo-selectors, but inline style plus theme tokens covers nearly everything.

When a frame renders, each widget’s style is resolved into a concrete computedStyle: $tokens are looked up against the active theme, focus/hover state is folded in, and the result is written into the ScreenBuffer as per-cell character + colors + attributes. Colors that the terminal can’t show natively are degraded (truecolor → 256 → 16) by the driver, so you author once in hex or tokens and let the backend match the device.