Theming
Components never name a brand colour. They are written in roles — bg-primary, text-ink,
border-control — and each role is a CSS variable that points at a colour. Change what the roles
point at and every component follows.
Three layers
Section titled “Three layers”| Layer | Variables | Who changes it |
|---|---|---|
| Palette | --zd-*, such as --zd-blue-600 |
Nobody: these are the ZitDevs brand values. |
| Semantic roles | --ui-*, such as --ui-primary, --ui-ink |
A product that needs its own colours. |
| Scales | --text-*, --space-*, --radius-*, motion… |
Nobody per product: they are the system’s rhythm and type. |
@zitdevs/tokens/tokens.css declares all three on :root, in the theme cascade layer, and
theme.css maps them to Tailwind utilities.
The ZitDevs mapping
Section titled “The ZitDevs mapping”The default each product starts from. Most roles take every colour utility (bg-, text-,
border-…). Text, field and line roles take one utility each, because the same word means a
different colour elsewhere: text-subtle is quiet text, border-subtle a faint line.
| Role | Utility | ZitDevs value |
|---|---|---|
--ui-primary | bg-primary, text-primary, border-primary… | --zd-blue · #2264dc |
--ui-primary-hover | bg-primary-hover, text-primary-hover, border-primary-hover… | --zd-blue-600 · #1b51b4 |
--ui-primary-active | bg-primary-active, text-primary-active, border-primary-active… | --zd-blue-700 · #153e8b |
--ui-primary-disabled | bg-primary-disabled, text-primary-disabled, border-primary-disabled… | --zd-blue-300 · #8fadf4 |
--ui-primary-soft | bg-primary-soft, text-primary-soft, border-primary-soft… | --zd-blue-50 · #eef3fe |
--ui-primary-soft-active | bg-primary-soft-active, text-primary-soft-active, border-primary-soft-active… | --zd-blue-100 · #d9e4fc |
--ui-primary-line | bg-primary-line, text-primary-line, border-primary-line… | --zd-blue-200 · #b4c9f8 |
--ui-primary-inverse | bg-primary-inverse, text-primary-inverse, border-primary-inverse… | --zd-blue-light · #5a8cff |
--ui-on-primary | bg-on-primary, text-on-primary, border-on-primary… | --zd-white · #ffffff |
--ui-accent | bg-accent, text-accent, border-accent… | --zd-orange · #f25928 |
--ui-accent-soft | bg-accent-soft, text-accent-soft, border-accent-soft… | --zd-orange-50 · #fef1ec |
--ui-accent-strong | bg-accent-strong, text-accent-strong, border-accent-strong… | --zd-orange-600 · #a9350c |
--ui-on-accent | bg-on-accent, text-on-accent, border-on-accent… | --zd-white · #ffffff |
--ui-ink | text-ink | --zd-navy · #0a0046 |
--ui-body | text-body | --zd-neutral-700 · #33312f |
--ui-muted | text-muted | --zd-neutral-500 · #6b6966 |
--ui-subtle | text-subtle | --zd-neutral-400 · #8e8c89 |
--ui-link | text-link | --zd-blue · #2264dc |
--ui-link-hover | text-link-hover | --zd-blue-600 · #1b51b4 |
--ui-on-inverse | bg-on-inverse, text-on-inverse, border-on-inverse… | --zd-white · #ffffff |
--ui-on-inverse-muted | bg-on-inverse-muted, text-on-inverse-muted, border-on-inverse-muted… | --zd-blue-100 · #d9e4fc |
--ui-on-inverse-quiet | bg-on-inverse-quiet, text-on-inverse-quiet, border-on-inverse-quiet… | --zd-cream · #faf7f0 |
--ui-surface | bg-surface | --zd-cream · #faf7f0 |
--ui-surface-raised | bg-surface-raised | --zd-white · #ffffff |
--ui-surface-sunken | bg-surface-sunken | --zd-neutral-50 · #f4f2ec |
--ui-surface-inverse | bg-surface-inverse | --zd-navy · #0a0046 |
--ui-surface-hover | bg-surface-hover | --zd-neutral-50 · #f4f2ec |
--ui-surface-active | bg-surface-active | --zd-neutral-100 · #e9e7e1 |
--ui-border-default | border-default | --zd-neutral-200 · #d7d5d0 |
--ui-border-subtle | border-subtle | --zd-neutral-100 · #e9e7e1 |
--ui-border-strong | border-strong | --zd-neutral-300 · #b7b5b1 |
--ui-border-control | border-control | --zd-neutral-400 · #8e8c89 |
--ui-danger | bg-danger, text-danger, border-danger… | --zd-danger · #d9430f |
--ui-danger-soft | bg-danger-soft, text-danger-soft, border-danger-soft… | --zd-danger-surface · #fef1ec |
--ui-danger-strong | bg-danger-strong, text-danger-strong, border-danger-strong… | --zd-orange-600 · #a9350c |
--ui-warning | bg-warning, text-warning, border-warning… | --zd-warning · #c97a0b |
--ui-warning-soft | bg-warning-soft, text-warning-soft, border-warning-soft… | --zd-warning-surface · #fbf1df |
--ui-success | bg-success, text-success, border-success… | --zd-success · #1e8e5a |
--ui-success-soft | bg-success-soft, text-success-soft, border-success-soft… | --zd-success-surface · #e7f4ee |
--ui-info | bg-info, text-info, border-info… | --zd-info · #2264dc |
--ui-info-soft | bg-info-soft, text-info-soft, border-info-soft… | --zd-info-surface · #eef3fe |
--ui-focus | shadow-focus (the ring) | --zd-blue · #2264dc |
--ui-focus-offset | shadow-focus (the ring) | --zd-white · #ffffff |
Three roles exist for contrast, not for variety: danger-strong and accent-strong are the steps
dark enough for text, since the danger and accent tones themselves fall under 4.5:1 on white; and
warning and success titles are set in ink for the same reason.
Give a product its own colours
Section titled “Give a product its own colours”Define the product’s palette, then point the roles at it. Override only the roles that change; the
rest keep the ZitDevs mapping. Put it after the kit’s import, on :root:
@import 'tailwindcss';@import '@zitdevs/tokens/theme.css';@source '../node_modules/@zitdevs/ui/dist';
:root { /* The product's palette. */ --acme-teal-50: #f0fdfa; --acme-teal-100: #ccfbf1; --acme-teal-200: #99f6e4; --acme-teal-300: #5eead4; --acme-teal-700: #0f766e; --acme-teal-800: #115e59; --acme-teal-900: #134e4a; --acme-amber: #d97706;
/* The kit's roles, in that palette. */ --ui-primary: var(--acme-teal-700); --ui-primary-hover: var(--acme-teal-800); --ui-primary-active: var(--acme-teal-900); --ui-primary-disabled: var(--acme-teal-300); --ui-primary-soft: var(--acme-teal-50); --ui-primary-soft-active: var(--acme-teal-100); --ui-primary-line: var(--acme-teal-200); --ui-accent: var(--acme-amber); --ui-focus: var(--acme-teal-700);}The same components, under that theme:
And under the ZitDevs mapping:
Rules that keep a theme sound
Section titled “Rules that keep a theme sound”- Override roles, never the palette.
--zd-*are the ZitDevs brand values; redefining--zd-blueto recolour a product would make the brand values lie. - Keep the contrast the kit was tested at. Text roles need 4.5:1 on the fields they sit on, and
border-controland the focus ring 3:1. The ZitDevs mapping is held to that by tests in@zitdevs/tokens; a product’s mapping is the product’s to check. - Composed roles resolve where they are declared. The focus rings (
--ui-focus-ring,--ui-focus-ring-danger,--ui-focus-ring-inset) are built from--ui-focus,--ui-focus-offsetand--ui-danger. Overriding the parts on:rootre-colours the rings; a theme scoped to an element declares the rings again in that scope, as the example above does. - A scoped theme is a class. Set the overrides on a selector instead of
:rootto theme one part of a page; every component inside it follows. - The logo never re-themes.
Logodraws the full-colour mark in the palette on purpose: the manual allows no recolouring. Usetone="mono"to set it in the text colour.
Coming from the website’s aliases
Section titled “Coming from the website’s aliases”The website’s own stylesheet has an older alias layer. Its names map onto the roles like this:
| Website alias | Role | Utility |
|---|---|---|
--surface-page |
--ui-surface |
bg-surface |
--surface-card |
--ui-surface-raised |
bg-surface-raised |
--surface-sunken |
--ui-surface-sunken |
bg-surface-sunken |
--surface-inverse |
--ui-surface-inverse |
bg-surface-inverse |
--surface-brand |
--ui-primary |
bg-primary |
--surface-brand-soft |
--ui-primary-soft |
bg-primary-soft |
--surface-accent |
--ui-accent |
bg-accent |
--surface-accent-soft |
--ui-accent-soft |
bg-accent-soft |
--text-heading |
--ui-ink |
text-ink |
--text-body |
--ui-body |
text-body |
--text-muted |
--ui-muted |
text-muted |
--text-subtle |
--ui-subtle |
text-subtle |
--text-on-brand |
--ui-on-primary |
text-on-primary |
--text-on-accent |
--ui-on-accent |
text-on-accent |
--text-link |
--ui-link |
text-link |
--text-link-hover |
--ui-link-hover |
text-link-hover |
--border-default |
--ui-border-default |
border-default |
--border-subtle |
--ui-border-subtle |
border-subtle |
--border-strong |
--ui-border-strong |
border-strong |
--border-control |
--ui-border-control |
border-control |
--border-brand |
--ui-primary |
border-primary |
--focus-ring |
--ui-focus-ring |
shadow-focus |
--text-inverse |
--ui-on-inverse-quiet |
text-on-inverse-quiet |
--border-focus |
--ui-primary-inverse |
border-primary-inverse |
--surface-brand-card |
--ui-surface-primary-raised |
bg-surface-primary-raised |
The cream the website sets on dark bands — resting tabs and languages, tags, a visual’s labels — is
--ui-on-inverse-quiet, apart from --ui-on-inverse (white), which is the band’s content and its
active state. The blue-light of --border-focus is the role every blue-light accent on a dark
field already takes, --ui-primary-inverse.
Composed roles
Section titled “Composed roles”Three kinds of role are mixed from others rather than pointing at a palette colour, so they follow
whatever the parts are themed to: the focus rings, and --ui-surface-primary-raised, a card on the
primary band (--ui-surface at 8% over --ui-primary, as drawn, made opaque so a pattern line
under it cannot take its text below 4.5:1). Like the rings, a theme scoped to an element declares it
again in that scope.