Theming

Tokens resolve against the theme, custom properties carry the identity, and three knobs let a person retune the whole of it.

Nothing in the library names a colour. A component asks for $background or $color12, the theme answers, and the answer differs between dark and light without the component knowing. A brand changes the answer by setting a custom property; a person changes it by moving a knob the ramps multiply by.

Tokens

A prop value starting with $ is a token. The colour ramp runs from surface to text: $color1 is the quietest ground, $color12 the loudest text.

TokenWhat it is
$backgroundThe page ground. Reads --background, so a brand’s stylesheet answers it.
$colorThe default text colour.
$color1 – $color12The neutral ramp: $color2/$color3 are raised surfaces, $color10/$color11 muted text, $color12 the loudest.
$borderColorThe hairline. Translucent, so it reads as a lighter line on any surface rather than an unrelated grey.
$accentBackground / $accentColorThe one loud control a page is allowed. Read --primary, so the accent knob reaches them.
defaultsecondarydestructiveoutline

The ladders

Size, space and radius are the same numbers on every host, and every rung carries a knob.

Type. fontSize="$n", never a number. Twelve rungs read the design ramp, --text-xs through --text-9xl, and the four between rungs interpolate their neighbours, so a person’s scale moves all sixteen and the ladder stays a ladder.

Rung$1$2$3$4$5$6$7$8$9$10$11$14
px111314151517212626324064

$1 is a label, $2 navigation and dense body, $3 base, $6 a section heading, $7 a page title, $8 and up display. Display rungs tighten their leading and track at −0.025em, which is the difference between type that is set and type that is merely large.

Space. The 4px ramp, and only the 4px ramp: $1 4, $2 8, $3 12, $4 16, $5 24, $6 32, $8 48, $10 64. Every step is multiplied by --density.

Radius. Four values. $1 and $2 are 6, a control; $3 and $4 are 8, an input or a row; $5 through $9 are 12, a panel; $10 and above are the pill. Everything but the pill is multiplied by --radius-scale.

Custom properties

The identity is a set of custom properties on :root, dark first, with .light retuning them. Hanzo stamps .t_dark or .t_light on the body and the CSS answers to both spellings, so the component tokens and the stylesheet name one theme.

:root {
--background: #0a0a0a;
--foreground: #e5e5e5;
--primary: #fafafa;
--border: rgb(255 255 255 / .10);
--ring: rgb(255 255 255 / .40);
--font-sans: 'Zen', ui-sans-serif, system-ui, sans-serif;
--font-mono: 'Zen Mono', ui-monospace, monospace;
}

The full set is --background, --foreground, --card, --popover, --primary, --secondary, --muted, --accent, --destructive, each with its -foreground, then --border, --input, --ring, --radius, --chart-1 through --chart-5, and the --sidebar-* family. They come from @hanzo/design and are composed into the stylesheet the root imports; a host that also mounts design’s own sheet follows it live.

A brand retunes by declaring the same names later in the cascade. A host that brings its own face binds --font-sans-provided rather than --font-sans, because the package’s binding sits in a layer and an unlayered declaration wins whatever its specificity.

The appearance knobs

@hanzo/design publishes three knobs that every ramp multiplies by, and @hanzo/appearance is the panel that sets them for a person:

KnobMoves
--type-scaleEvery --text-* rung, and with it every fontSize="$n".
--type-ratioHow far apart the rungs sit.
--densityEvery --space-* step, and every gui gap and padding.
--primary / --accentThe one loud colour.
--font-sansThe reading face: the brand’s, the system’s, a serif, or mono.
--container-max / --container-prose / --container-wideThe three measures a page lays out to.

They land as inline properties on html, which is :root, so they win over every stylesheet without a selector to out-specify, and they reach the components because gui resolves a size in JavaScript as var(--text-base) and the var still reads the cascade at the element. An axis nobody set is absent rather than neutral: writing 1 would look the same and silently outrank a brand that published its own scale.

import { Appearance } from '@hanzo/appearance'
import { bootScript } from '@hanzo/appearance/state'
// In <head>, so the first paint is already correct.
<script dangerouslySetInnerHTML={{ __html: bootScript() }} />
// Wherever the product keeps settings.
<Appearance />

Hanzo re-applies the stored preference on every mount, so a choice made on one page holds on the next. The control in this site’s header is that panel.