Skip to content

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.

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 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.

RoleUtilityZitDevs value
--ui-primarybg-primary, text-primary, border-primary…--zd-blue · #2264dc
--ui-primary-hoverbg-primary-hover, text-primary-hover, border-primary-hover…--zd-blue-600 · #1b51b4
--ui-primary-activebg-primary-active, text-primary-active, border-primary-active…--zd-blue-700 · #153e8b
--ui-primary-disabledbg-primary-disabled, text-primary-disabled, border-primary-disabled…--zd-blue-300 · #8fadf4
--ui-primary-softbg-primary-soft, text-primary-soft, border-primary-soft…--zd-blue-50 · #eef3fe
--ui-primary-soft-activebg-primary-soft-active, text-primary-soft-active, border-primary-soft-active…--zd-blue-100 · #d9e4fc
--ui-primary-linebg-primary-line, text-primary-line, border-primary-line…--zd-blue-200 · #b4c9f8
--ui-primary-inversebg-primary-inverse, text-primary-inverse, border-primary-inverse…--zd-blue-light · #5a8cff
--ui-on-primarybg-on-primary, text-on-primary, border-on-primary…--zd-white · #ffffff
--ui-accentbg-accent, text-accent, border-accent…--zd-orange · #f25928
--ui-accent-softbg-accent-soft, text-accent-soft, border-accent-soft…--zd-orange-50 · #fef1ec
--ui-accent-strongbg-accent-strong, text-accent-strong, border-accent-strong…--zd-orange-600 · #a9350c
--ui-on-accentbg-on-accent, text-on-accent, border-on-accent…--zd-white · #ffffff
--ui-inktext-ink--zd-navy · #0a0046
--ui-bodytext-body--zd-neutral-700 · #33312f
--ui-mutedtext-muted--zd-neutral-500 · #6b6966
--ui-subtletext-subtle--zd-neutral-400 · #8e8c89
--ui-linktext-link--zd-blue · #2264dc
--ui-link-hovertext-link-hover--zd-blue-600 · #1b51b4
--ui-on-inversebg-on-inverse, text-on-inverse, border-on-inverse…--zd-white · #ffffff
--ui-on-inverse-mutedbg-on-inverse-muted, text-on-inverse-muted, border-on-inverse-muted…--zd-blue-100 · #d9e4fc
--ui-on-inverse-quietbg-on-inverse-quiet, text-on-inverse-quiet, border-on-inverse-quiet…--zd-cream · #faf7f0
--ui-surfacebg-surface--zd-cream · #faf7f0
--ui-surface-raisedbg-surface-raised--zd-white · #ffffff
--ui-surface-sunkenbg-surface-sunken--zd-neutral-50 · #f4f2ec
--ui-surface-inversebg-surface-inverse--zd-navy · #0a0046
--ui-surface-hoverbg-surface-hover--zd-neutral-50 · #f4f2ec
--ui-surface-activebg-surface-active--zd-neutral-100 · #e9e7e1
--ui-border-defaultborder-default--zd-neutral-200 · #d7d5d0
--ui-border-subtleborder-subtle--zd-neutral-100 · #e9e7e1
--ui-border-strongborder-strong--zd-neutral-300 · #b7b5b1
--ui-border-controlborder-control--zd-neutral-400 · #8e8c89
--ui-dangerbg-danger, text-danger, border-danger…--zd-danger · #d9430f
--ui-danger-softbg-danger-soft, text-danger-soft, border-danger-soft…--zd-danger-surface · #fef1ec
--ui-danger-strongbg-danger-strong, text-danger-strong, border-danger-strong…--zd-orange-600 · #a9350c
--ui-warningbg-warning, text-warning, border-warning…--zd-warning · #c97a0b
--ui-warning-softbg-warning-soft, text-warning-soft, border-warning-soft…--zd-warning-surface · #fbf1df
--ui-successbg-success, text-success, border-success…--zd-success · #1e8e5a
--ui-success-softbg-success-soft, text-success-soft, border-success-soft…--zd-success-surface · #e7f4ee
--ui-infobg-info, text-info, border-info…--zd-info · #2264dc
--ui-info-softbg-info-soft, text-info-soft, border-info-soft…--zd-info-surface · #eef3fe
--ui-focusshadow-focus (the ring)--zd-blue · #2264dc
--ui-focus-offsetshadow-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.

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:

app/globals.css
@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:

Acme

Read the guide

Only the roles changed; the components did not.

And under the ZitDevs mapping:

ZitDevs

Read the guide

Only the roles changed; the components did not.

  • Override roles, never the palette. --zd-* are the ZitDevs brand values; redefining --zd-blue to 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-control and 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-offset and --ui-danger. Overriding the parts on :root re-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 :root to theme one part of a page; every component inside it follows.
  • The logo never re-themes. Logo draws the full-colour mark in the palette on purpose: the manual allows no recolouring. Use tone="mono" to set it in the text colour.

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.

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.