@krizaka/tokens
The Krizaka semantic design tokens. One vocabulary, --kz-*, written once in DTCG
JSON and compiled to CSS (dark, light, media invariants), typed constants and React Native themes. It is the vocabulary
krizaka.com already runs on (1 974 usages, zero raw palette colour), now shared by every product.
The rule
A component never reads a palette colour, only a role.
bg-zinc-950/70 light:bg-white→var(--kz-surface-1);text-zinc-400 light:text-slate-500→var(--kz-text-secondary);bg-black/60 text-whiteon an image →var(--kz-scrim)+var(--kz-text-on-media).
A theme is a set of values, never a set of classes. A component is written once against the roles below; dark,
light and a product theme (an override of --kz-accent, --kz-font-display…) only change the values. No light:
variant, no dark: variant, no zinc-* / slate-* / #hex in a component.
Install
npm install @krizaka/tokens
| Entry | For | Gives |
|---|---|---|
@krizaka/tokens/tokens.css | Web (imported once, usually through @krizaka/tailwind) | The --kz-* custom properties. |
@krizaka/tokens | TS / JS on the web (canvas, charts, e-mails, inline styles) | tokens (var(--kz-…) references, camelCase keys), values.dark / values.light (raw CSS, aliases resolved), TokenName. |
@krizaka/tokens/native | React Native / Expo | themes.dark / themes.light (hex, rgba() when translucent), radius (dp), typography, motion.ease, type Theme; brands.<id> (ThemeProvider overrides); marks.<id> (the mark's emblem gradient, [offset, #rrggbb] stops). |
@krizaka/tokens/brands/<id>.css | A product app (krizaka, orazaka, yomizaka, akinaza), after the tokens | The brand's accent family and section gradient, dark and light. |
@krizaka/tokens/brands/scoped.css | A page showing several brands | .brand-krizaka, .brand-orazaka, .brand-yomizaka, .brand-akinaza (the paused products' scopes still ship, deprecated). |
Brands: one colour per brand, its marks, icons and page language are in BRAND.md.
@import "@krizaka/tokens/tokens.css";
.card { background: var(--kz-surface-1); border: 1px solid var(--kz-border-default); border-radius: var(--kz-radius-lg); }
import { tokens, values } from "@krizaka/tokens";
element.style.color = tokens.textSecondary; // "var(--kz-text-secondary)" — follows the theme
chart.gridColor = values.dark.borderSubtle; // "hsl(0 0% 100% / 0.06)"
import { themes, radius, type Theme } from "@krizaka/tokens/native";
const t: Theme = scheme === "light" ? themes.light : themes.dark;
<View style={{ backgroundColor: t.surface1, borderRadius: radius.lg }} />;
Themes
| Selector | Gets |
|---|---|
:root, .theme-dark | Dark — the default. .theme-dark re-declares the dark values, so a player, an editor or a media stays dark inside a light page without one light: class in its subtree. |
html.light | Light. Toggle the light class on <html>. |
.theme-light | Light on a subtree (a theme preview inside a dark page): the light values and the aliases, the counterpart of .theme-dark. |
:root (invariants) | Identical in both themes: what sits on a media (scrims, text on media, dialog overlay), statuses, on-accent, radii, easing, font stacks. |
color-scheme follows the theme, so scrollbars and native controls match.
Roles
Values are the CSS written in dist/tokens.css; → marks an alias, which follows its target in every theme and
under every product override.
Colour
| Token | Key | Usage | Dark | Light |
|---|---|---|---|---|
--kz-surface-0 | surface0 | Page background: the slate (dark), the paper (light) | hsl(150 12% 9.5%) | hsl(100 14% 96%) |
--kz-surface-1 | surface1 | Card, panel | hsl(240 5% 9%) | hsl(0 0% 100%) |
--kz-surface-2 | surface2 | Raised element, field, hover | hsl(240 5% 13%) | hsl(240 6% 95%) |
--kz-surface-3 | surface3 | Active element, chip, track | hsl(240 4% 17%) | hsl(240 5% 90%) |
--kz-media | media | A media's background before it loads | hsl(240 5% 11%) | hsl(240 6% 92%) |
--kz-border-subtle | borderSubtle | Quiet separator | hsl(0 0% 100% / 0.06) | hsl(240 6% 0% / 0.06) |
--kz-border-default | borderDefault | Card and field outline | hsl(0 0% 100% / 0.10) | hsl(240 6% 0% / 0.10) |
--kz-border-strong | borderStrong | Hover outline, non-keyboard focus | hsl(0 0% 100% / 0.16) | hsl(240 6% 0% / 0.16) |
--kz-text-primary | textPrimary | Headings, body text (≥ 4.5:1 on every surface): chalk, ink | hsl(105 17% 93%) | hsl(212 49% 22%) |
--kz-text-secondary | textSecondary | Supporting text, labels (≥ 4.5:1 on every surface) | hsl(240 4% 64%) | hsl(240 4% 41%) |
--kz-text-muted | textMuted | Metadata, placeholders, disabled (≥ 3:1 on every surface) | hsl(240 4% 47%) | hsl(240 4% 52%) |
--kz-text-on-media | textOnMedia | Text on an image or a video | hsl(0 0% 100%) | = dark |
--kz-accent | accent | Primary action, link, selection — the product's identity | hsl(217 92% 53%) | hsl(217 92% 50%) |
--kz-accent-hover | accentHover | Accent on hover | hsl(217 88% 45%) | hsl(217 88% 42%) |
--kz-accent-soft | accentSoft | Tinted background (selection, badge) | hsl(217 92% 53% / 0.10) | hsl(217 92% 50% / 0.08) |
--kz-accent-text | accentText | The accent as text (link, active label): ≥ 4.5:1 on every surface | hsl(217 92% 68%) | hsl(217 92% 42%) |
--kz-accent-2 | accent2 | Second accent (gradients) | → accent | → accent |
--kz-on-accent | onAccent | Text and icon on the accent (≥ 4.5:1) | hsl(0 0% 100%) | = dark |
--kz-ring | ring | Keyboard focus ring | → accent | → accent |
--kz-brand-gradient-from / -via / -to | brandGradientFrom… | Section gradient of the brand — flat since Cahier (ADR-004): the page surface; text reads at AA on every stop. -to is an alias of surface-0: an app that changes its page surface keeps a seamless section end | hsl(222 45% 10%) → hsl(232 24% 7%) → surface-0 | hsl(214 95% 93%) → hsl(220 60% 97%) → surface-0 |
--kz-success | success | Status: success — fills, borders, dots, icons (≥ 3:1) | hsl(160 84% 39%) | = dark |
--kz-success-text | successText | Success as small text: ≥ 4.5:1 on every surface | hsl(158 64% 52%) | hsl(160 84% 24%) |
--kz-warning | warning | Status: warning | hsl(38 92% 50%) | = dark |
--kz-warning-text | warningText | Warning as small text: ≥ 4.5:1 on every surface | hsl(38 92% 55%) | hsl(32 95% 30%) |
--kz-danger | danger | Status: error, destructive action | hsl(0 84% 60%) | = dark |
--kz-danger-text | dangerText | Error as small text (a field's message, an urgent countdown): ≥ 4.5:1 on every surface | hsl(0 91% 71%) | hsl(0 72% 40%) |
--kz-info | info | Status: information | → accent | → accent |
--kz-info-text | infoText | Information as small text (a brand role) | → accent-text | → accent-text |
--kz-scrim | scrim | Veil behind a badge or a caption on an image | hsl(0 0% 0% / 0.60) | = dark |
--kz-scrim-strong | scrimStrong | Stronger veil (long text on an image) | hsl(0 0% 0% / 0.80) | = dark |
--kz-overlay | overlay | Dialog backdrop | hsl(0 0% 0% / 0.70) | = dark |
Shape, depth, motion, type
| Token | Key | Usage | Dark | Light |
|---|---|---|---|---|
--kz-radius-sm | radiusSm | Field, chip | 2px | = dark |
--kz-radius-md | radiusMd | Button, menu | 2px | = dark |
--kz-radius-lg | radiusLg | Card (square paper) | 0px | = dark |
--kz-radius-xl | radiusXl | Dialog, sheet | 0px | = dark |
--kz-radius-full | radiusFull | Pill, avatar | 9999px | = dark |
--kz-shadow-sm | shadowSm | Field, button at rest (web only): a 1 px ink line, no blur | 0 1px 0 hsl(150 30% 3% / 0.6) | 0 1px 0 hsl(212 49% 24% / 0.12) |
--kz-shadow-md | shadowMd | Menu, popover (web only): a sheet on a sheet | 0 1px 0 …, 4px 5px 0 -2px hsl(150 30% 4% / 0.55) | 0 1px 0 …, 4px 5px 0 -2px hsl(212 40% 30% / 0.14) |
--kz-shadow-lg | shadowLg | Dialog, sheet (web only) | 0 1px 0 …, 6px 8px 0 -2px … | 0 1px 0 …, 6px 8px 0 -2px … |
--kz-ease | ease | The ink stroke: the default curve (Cahier) | cubic-bezier(0.45, 0, 0.2, 1) | = dark |
--kz-font-sans | fontSans | Interface and running text | "Atkinson Hyperlegible Next", system-ui, sans-serif | = dark |
--kz-font-display | fontDisplay | Headings; a product theme may override it | → font-sans | → font-sans |
--kz-font-mono | fontMono | Tags, captions, footnotes, code | "Courier Prime", "Courier New", monospace | = dark |
The paper (Cahier, ADR-004)
| Token | Native | Role | Dark (slate) | Light (paper) |
|---|---|---|---|---|
--kz-paper-rule-major / -minor | paperRuleMajor… | The Seyès ruling: a strong line every 32 px, three fine lines | chalk at 9 % / 4.5 % | ink blue at 24 % / 10 % |
--kz-paper-rule-margin | paperRuleMargin | The red margin line, section numbers in the margin (≥ 3:1) | hsl(8 64% 66%) | hsl(5 56% 47%) |
--kz-paper-tag / -tag-edge / -tag-ink | paperTag… | The specimen tag (manila), its edge and eyelet, its typed text (≥ 4.5:1) | slate tag, chalk ink | manila, near-black ink |
--kz-paper-module / --kz-paper-line | — | The ruling's square and line | 32px / 8px | = dark |
The material (BRAND.md §6)
| Token | Key | Usage | Dark | Light |
|---|---|---|---|---|
--kz-radius-xs, --kz-radius-2xl | radiusXs, radius2xl | Checkbox, key, cut badge · sheet | 2px, 0px | = dark |
--kz-shadow-raised · -floating · -overlay | shadowRaised… | Raised (button, card) · floating (menu, tooltip) · over the page (dialog, toast) — ink-tinted, 2–3 layers; native: shadows.<theme>.raised (boxShadow) | hsl(240 30% 2% / …) | hsl(232 25% 25% / …) |
--kz-surface-sheen | surfaceSheen | The top of a raised surface (bg-sheen) | hsl(0 0% 100% / 0.045) | hsl(0 0% 100% / 0.7) |
--kz-fill-shade | fillShade | The bottom of an accent or status fill (bg-fill-shade) | hsl(0 0% 0% / 0.12) | = dark |
--kz-edge-light · -on-fill · -well | edgeLight… | The 1-px top edge of a raised surface · of a fill · the inner shade of a well | / 0.07 · / 0.22 · ink / 0.5 | / 0.9 · = · ink / 0.08 |
--kz-glass-bg · -border · -blur | glassBg… | Frosted glass: menus, popovers, dialogs, toasts (88 %: text AA over anything) | hsl(240 5% 12% / 0.88) | hsl(0 0% 100% / 0.88) |
--kz-success-soft · warning-soft · danger-soft | successSoft… | Status tints: badges, alerts, rows | / 0.14 | / 0.10–0.12 |
--kz-ring-width · -offset · -halo | ringWidth… | The signature focus ring (focus-ring) | 2px · 2px · 4px | = dark |
--kz-control-sm · md · lg · field | controlSm… | Control heights; [data-density="compact"] tightens them | 32 · 40 · 48 · 44px | compact 28 · 34 · 40 · 36px |
--kz-cut-shape · --kz-cut-size | cutShape, cutSize | The griffe: the 45° cut corner (corner-shape); round turns it off; native: cut.size | bevel · 10px | = dark |
--kz-ease-spring | easeSpring | A short spring: a press, a toggle, a selection; native motion.spring | cubic-bezier(0.34, 1.36, 0.64, 1) | = dark |
--kz-duration-instant · fast · base | durationFast… | Micro-interactions ≤ 200 ms; 0 ms under prefers-reduced-motion; native motion.duration | 90 · 150 · 200ms | = dark |
Contrast is tested, not assumed: pnpm turbo run check fails if text-primary / text-secondary drop under 4.5:1 or
text-muted under 3:1 on any surface, or on-accent under 4.5:1 on the accent, in either theme (WCAG 2.x).
No accent can carry white text at 4.5:1 and be read at 4.5:1 as text on surface-0: the accent favours the text
set on it, and accent-text (text-fg-accent) is the accent for words — tested at 4.5:1 on every surface.
Add or change a token
- Source — edit
src/tokens/<category>.tokens.json(DTCG:$type,$value,$description).$valueis the dark value (the default); the light value goes in"$extensions": { "com.krizaka": { "light": … } }. No extension = the same value in both themes.- An alias is
"{group.token}"("{accent.$root}"for a group's own token); it compiles tovar(--kz-…)and may not carry a light value — it follows its target. - The CSS name is the path joined with
-(text.on-media→--kz-text-on-media), the JS key its camelCase (textOnMedia). Colours arehsl(); radii arepx(the native output converts them to dp).
- Build —
pnpm --filter @krizaka/tokens buildwritesdist/(never editdist/), thenpnpm --filter @krizaka/tokens test: update thetokens.csssnapshot only when the change is intended (vitest run -u), and keep the contrast tests green. - Changeset —
pnpm changeset: a new token is a minor, a changed value at least a minor (it changes every product), a removed or renamed token a major.@krizaka/tokensand@krizaka/uiare released together (fixedgroup).