Skip to content

Theming

A theme is a named set of semantic colors. Widgets reference those colors by token ($primary, $surface, …) rather than literal hex, so one widget tree re-colors itself completely when the active theme changes — no per-widget edits.

Use these in any color / background (and anywhere a style value is a string, via $name or var(--name) — see Styling):

TokenRole
$backgroundapp backdrop
$foregrounddefault text
$surfaceraised surface (cards, panels)
$panela second elevation above surface
$primaryprimary accent / interactive
$secondarysecondary accent
$accenttertiary accent / highlights
$successpositive state
$warningcaution state
$errorerror / destructive state
$borderborder lines
$dimmedde-emphasized text

Syntax-highlighting tokens ($keyword, $string, $number, $comment, …) and UI accents ($focus, $selectionBg) are also defined; the Markdown and Diff widgets use them automatically.

ztui ships a large set of popular palettes, including default-dark (the default) and default-light, plus catppuccin-mocha / -macchiato / -frappe / -latte, nord, dracula, gruvbox-dark / -light, tokyo-night, one-dark, rose-pine, monokai, everforest, solarized-dark / -light, cobalt2, poimandres, kanagawa, github-dark, horizon, and nightfly.

List them at runtime:

import { ThemeManager } from "@huyz0/ztui";
const tm = ThemeManager.getInstance();
tm.listThemes().map((t) => t.name);
import { ThemeManager } from "@huyz0/ztui";
ThemeManager.getInstance().setTheme("tokyo-night");

The app subscribes to theme changes and re-renders the whole tree, so every $token updates live. (An unknown name is ignored with a warning.)

Mount <ThemePalette> once near your app root and press Ctrl+T for a built-in visual picker. It lays every registered theme out as a card painted in its own colors — the palette swatches plus a tiny live example (a button, body text, and status dots) — so you can compare them at a glance:

A grid of ztui theme cards, each rendered in its own colors with a palette strip and a sample button

import { ThemePalette } from "@huyz0/ztui/react";
<Dock>
<ThemePalette /> {/* Ctrl+T; override with toggleKey="…" */}
</Dock>;
  • Browse with the arrows, or scroll/drag the grid with the mouse — the card you land on previews live across the whole app.
  • Enter or click applies a theme but keeps the picker open, so you can see it in context before committing.
  • Esc closes — keeping a theme you applied, or reverting an un-committed preview back to the one you opened with.

Bind it like a controlled input: pass the saved theme name in as value, and write the chosen one back from onSelect (to localStorage, a config file, …).

const [theme, setTheme] = useState(() => loadSavedTheme() ?? "default-dark");
<ThemePalette
value={theme}
onSelect={(t) => {
setTheme(t.name);
saveTheme(t.name);
}}
/>;

Set the theme prop on any widget to re-theme just that subtree — handy for a preview pane or a callout in a different palette. Tokens in descendants resolve against the nearest ancestor theme:

<VBox theme="dracula">
<Label style={{ color: "$primary" }}>themed independently of the app</Label>
</VBox>

Register a Theme (just a name and a colors map) and switch to it:

import { ThemeManager } from "@huyz0/ztui";
const tm = ThemeManager.getInstance();
tm.register({
name: "brand",
colors: {
primary: "#7c5cff",
secondary: "#22d3ee",
background: "#0b0b12",
foreground: "#e7e7ef",
surface: "#15151f",
panel: "#1d1d2a",
accent: "#f0abfc",
success: "#34d399",
warning: "#fbbf24",
error: "#fb7185",
border: "#2a2a3a",
},
});
tm.setTheme("brand");

Only the core tokens are required; anything you omit falls back sensibly. To spin a variant off an existing palette, use deriveTheme:

import { deriveTheme, ThemeManager } from "@huyz0/ztui";
const tm = ThemeManager.getInstance();
const dimmer = deriveTheme(tm.getTheme("nord")!, "nord-dim", { adjustLightness: -8 });
tm.register(dimmer);

ztui honours the NO_COLOR convention. When NO_COLOR (any value) or ZTUI_NO_COLOR is set in the environment, the terminal renderer drops all foreground/background colour and emits only the monochrome attributes (bold, dim, italic, underline, strikethrough, reverse) plus hyperlinks; FORCE_COLOR overrides back to colour.

You can also toggle it at runtime via the global colorMode flag — call App.refresh() afterwards so the next frame re-emits every cell:

import { App, colorMode } from "@huyz0/ztui";
colorMode.set(false); // monochrome
App.instance?.refresh();

This mirrors the motion flag (which honours NO_MOTION / ZTUI_REDUCED_MOTION to settle ambient animations).