Tokens and theming
Every colour, space, radius, type size and duration in Zeno is a design token.
The tokens live in @usezeno/tokens as JSON, and Style Dictionary builds them
into a stylesheet and a JavaScript module. There are 279 of them.
The tables on this page are built from that package and drawn with the tokens
themselves, so they always show the current values.
Load the stylesheet
Load the stylesheet once per page, before any Zeno element renders:
import '@usezeno/tokens/css';
It defines each token as a custom property that starts with --zeno-. The
Zeno elements read those properties and carry no fallback values. An element
on a page without the stylesheet has no colours or spacing.
Light and dark
Light is the default theme. The stylesheet switches to dark when the operating
system asks for dark. To override that, set data-theme:
| Markup | Result |
|---|---|
<html> | Follows the operating system |
<html data-theme="light"> | Light, whatever the operating system says |
<html data-theme="dark"> | Dark, whatever the operating system says |
data-theme also works on any element, and themes that element and
everything inside it:
<section data-theme="dark">
<zeno-button>Dark, on a light page</zeno-button>
</section>
@usezeno/core exports helpers that set the attribute for you:
import { getTheme, setTheme, onSystemThemeChange } from '@usezeno/core';
setTheme('dark'); // sets data-theme="dark" on <html>
setTheme('system'); // removes the attribute
getTheme(); // 'light' or 'dark', after resolving 'system'
const stop = onSystemThemeChange(theme => console.log(theme));
Colour
Colour tokens change with the theme. Each row shows the token in the light theme and in the dark theme, whatever the theme of this page is.
Background
| Token | Light | Dark |
|---|---|---|
--zeno-color-background-page | #f9fafd | #101215 |
--zeno-color-background-surface | #ffffff | #1a1c20 |
--zeno-color-background-subtle | #f2f3f6 | #25282c |
--zeno-color-background-muted | #e5e6e9 | #33353a |
--zeno-color-background-hover | rgba(16, 18, 21, 0.06) | rgba(255, 255, 255, 0.08) |
--zeno-color-background-active | rgba(16, 18, 21, 0.1) | rgba(255, 255, 255, 0.12) |
--zeno-color-background-selected | #f2f7fe | #001b47 |
--zeno-color-background-disabled | #f2f3f6 | #25282c |
--zeno-color-background-inverse | #1a1c20 | #f2f3f6 |
--zeno-color-background-scrim | rgba(16, 18, 21, 0.5) | rgba(0, 0, 0, 0.65) |
Text
| Token | Light | Dark |
|---|---|---|
--zeno-color-text-primary | #101215 | #f9fafd |
--zeno-color-text-secondary | #4d5054 | #d1d4da |
--zeno-color-text-tertiary | #66696e | #a8abb0 |
--zeno-color-text-placeholder | #66696e | #a8abb0 |
--zeno-color-text-disabled | #a8abb0 | #66696e |
--zeno-color-text-inverse | #f9fafd | #101215 |
--zeno-color-text-invalid | #aa0216 | #fdaca3 |
Border
| Token | Light | Dark |
|---|---|---|
--zeno-color-border-subtle | #e5e6e9 | #33353a |
--zeno-color-border-default | #d1d4da | #4d5054 |
--zeno-color-border-strong | #a8abb0 | #66696e |
--zeno-color-border-control | #85888d | #85888d |
--zeno-color-border-control-hover | #66696e | #a8abb0 |
--zeno-color-border-disabled | #d1d4da | #4d5054 |
--zeno-color-border-invalid | #c72d2e | #f57a71 |
Focus, links and selection
| Token | Light | Dark |
|---|---|---|
--zeno-color-focus-ring | #136adc | #68a5ff |
| Token | Light | Dark |
|---|---|---|
--zeno-color-link-default | #0053b7 | #a0c6fe |
--zeno-color-link-hover | #003f8e | #c8deff |
--zeno-color-link-active | #002c6a | #e3eefe |
--zeno-color-link-visited | #732eac | #d3b4f7 |
| Token | Light | Dark |
|---|---|---|
--zeno-color-selection-background | #c8deff | #003f8e |
--zeno-color-selection-text | #101215 | #ffffff |
Colour families
The colour families are accent, neutral, success, warning, danger
and info. Each one has the same eight tokens:
| Token | Use |
|---|---|
solid | A filled control or shape |
solidHover, solidActive | The same fill when hovered or pressed |
onSolid | Text and icons on the solid fill |
subtle, subtleHover | A tinted background |
border | An outline in the family colour |
text | Coloured text on a page or subtle background |
Accent
| Token | Light | Dark |
|---|---|---|
--zeno-color-accent-solid | #136adc | #68a5ff |
--zeno-color-accent-solid-hover | #0053b7 | #a0c6fe |
--zeno-color-accent-solid-active | #003f8e | #c8deff |
--zeno-color-accent-on-solid | #ffffff | #101215 |
--zeno-color-accent-subtle | #f2f7fe | #001b47 |
--zeno-color-accent-subtle-hover | #e3eefe | #002c6a |
--zeno-color-accent-border | #3485f9 | #3485f9 |
--zeno-color-accent-text | #0053b7 | #a0c6fe |
Neutral
| Token | Light | Dark |
|---|---|---|
--zeno-color-neutral-solid | #33353a | #e5e6e9 |
--zeno-color-neutral-solid-hover | #4d5054 | #f2f3f6 |
--zeno-color-neutral-solid-active | #101215 | #ffffff |
--zeno-color-neutral-on-solid | #ffffff | #101215 |
--zeno-color-neutral-subtle | #f2f3f6 | #25282c |
--zeno-color-neutral-subtle-hover | #e5e6e9 | #33353a |
--zeno-color-neutral-border | #85888d | #85888d |
--zeno-color-neutral-text | #4d5054 | #d1d4da |
Success
| Token | Light | Dark |
|---|---|---|
--zeno-color-success-solid | #058644 | #66ba7f |
--zeno-color-success-solid-hover | #016c35 | #9fd3ac |
--zeno-color-success-solid-active | #005226 | #c7e6ce |
--zeno-color-success-on-solid | #ffffff | #101215 |
--zeno-color-success-subtle | #eefbf1 | #00260e |
--zeno-color-success-subtle-hover | #dff3e3 | #003b1a |
--zeno-color-success-border | #29a259 | #29a259 |
--zeno-color-success-text | #016c35 | #9fd3ac |
Warning
| Token | Light | Dark |
|---|---|---|
--zeno-color-warning-solid | #9d6000 | #d6954a |
--zeno-color-warning-solid-hover | #7e4d00 | #e6bb8e |
--zeno-color-warning-solid-active | #603a01 | #f1d8bd |
--zeno-color-warning-on-solid | #ffffff | #101215 |
--zeno-color-warning-subtle | #fef5ec | #2e1800 |
--zeno-color-warning-subtle-hover | #fbead8 | #472800 |
--zeno-color-warning-border | #bf7601 | #bf7601 |
--zeno-color-warning-text | #7e4d00 | #e6bb8e |
Danger
| Token | Light | Dark |
|---|---|---|
--zeno-color-danger-solid | #c72d2e | #f57a71 |
--zeno-color-danger-solid-hover | #aa0216 | #fdaca3 |
--zeno-color-danger-solid-active | #83010e | #fed0ca |
--zeno-color-danger-on-solid | #ffffff | #101215 |
--zeno-color-danger-subtle | #fef4f3 | #410002 |
--zeno-color-danger-subtle-hover | #ffe6e4 | #610007 |
--zeno-color-danger-border | #e54c48 | #e54c48 |
--zeno-color-danger-text | #aa0216 | #fdaca3 |
Info
| Token | Light | Dark |
|---|---|---|
--zeno-color-info-solid | #037c9a | #58b2d0 |
--zeno-color-info-solid-hover | #00647c | #98cee1 |
--zeno-color-info-solid-active | #014c5f | #c4e2ee |
--zeno-color-info-on-solid | #ffffff | #101215 |
--zeno-color-info-subtle | #edf9fe | #00222d |
--zeno-color-info-subtle-hover | #dcf1f9 | #003645 |
--zeno-color-info-border | #0398bb | #0398bb |
--zeno-color-info-text | #00647c | #98cee1 |
Palette
The theme tokens above point at these steps. The palette is the same in both themes. Use a theme token in your own styles where one fits, so your colours follow the theme.
States
| State | Tokens |
|---|---|
| Hover | color.background.hover, color.border.controlHover, solidHover, subtleHover |
| Active | color.background.active, solidActive |
| Selected | color.background.selected |
| Focus | color.focus.ring, focus.ring.width, focus.ring.offset |
| Disabled | color.text.disabled, color.background.disabled, color.border.disabled |
| Invalid | color.text.invalid, color.border.invalid |
Change the accent
The theme tokens point at palette steps, so you can re-theme by overriding a few properties after the Zeno stylesheet:
:root {
--zeno-color-accent-solid: #0f766e;
--zeno-color-accent-solid-hover: #115e59;
--zeno-color-accent-solid-active: #134e4a;
}
Zeno checks its own colour pairs for contrast. It cannot check yours, so test any colour you override against the text and backgrounds it appears with.
Fonts
The font tokens name IBM Plex Sans and IBM Plex Mono first, followed by system fonts. Zeno ships no font files. Load IBM Plex yourself if you want it. If you do not, the system fonts apply.
Contrast
The token package tests a list of colour pairs against WCAG 2.2 AA in both
themes: 4.5:1 for text, and 3:1 for control boundaries, focus rings and filled
shapes. The list is in packages/tokens/test/contrast-pairs.mjs, and the test
runs as part of pnpm test.
Disabled controls and decorative separators are not in the list. WCAG exempts them from these two criteria.
Type
Families
| Token | Value | Sample |
|---|---|---|
--zeno-font-family-sans | IBM Plex Sans,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif | A small black dog 0123 |
--zeno-font-family-mono | IBM Plex Mono,ui-monospace,SFMono-Regular,Menlo,Consolas,Liberation Mono,monospace | A small black dog 0123 |
Sizes
| Token | Value | Sample |
|---|---|---|
--zeno-font-size-2xs | 0.6875rem | Zeno |
--zeno-font-size-xs | 0.75rem | Zeno |
--zeno-font-size-sm | 0.875rem | Zeno |
--zeno-font-size-md | 1rem | Zeno |
--zeno-font-size-lg | 1.125rem | Zeno |
--zeno-font-size-xl | 1.25rem | Zeno |
--zeno-font-size-2xl | 1.5rem | Zeno |
--zeno-font-size-3xl | 1.875rem | Zeno |
--zeno-font-size-4xl | 2.25rem | Zeno |
--zeno-font-size-5xl | 3rem | Zeno |
--zeno-font-size-6xl | 3.75rem | Zeno |
Weights
| Token | Value | Sample |
|---|---|---|
--zeno-font-weight-regular | 400 | A small black dog |
--zeno-font-weight-medium | 500 | A small black dog |
--zeno-font-weight-semibold | 600 | A small black dog |
--zeno-font-weight-bold | 700 | A small black dog |
Line heights
| Token | Value |
|---|---|
--zeno-font-line-height-none | 1 |
--zeno-font-line-height-tight | 1.25 |
--zeno-font-line-height-snug | 1.375 |
--zeno-font-line-height-normal | 1.5 |
--zeno-font-line-height-relaxed | 1.625 |
Letter spacing
| Token | Value | Sample |
|---|---|---|
--zeno-font-letter-spacing-tight | -0.02em | A small black dog |
--zeno-font-letter-spacing-normal | 0 | A small black dog |
--zeno-font-letter-spacing-wide | 0.02em | A small black dog |
--zeno-font-letter-spacing-wider | 0.05em | A small black dog |
Spacing
| Token | Value | Sample |
|---|---|---|
--zeno-spacing-0 | 0 | |
--zeno-spacing-0-5 | 0.125rem | |
--zeno-spacing-1 | 0.25rem | |
--zeno-spacing-1-5 | 0.375rem | |
--zeno-spacing-2 | 0.5rem | |
--zeno-spacing-2-5 | 0.625rem | |
--zeno-spacing-3 | 0.75rem | |
--zeno-spacing-3-5 | 0.875rem | |
--zeno-spacing-4 | 1rem | |
--zeno-spacing-5 | 1.25rem | |
--zeno-spacing-6 | 1.5rem | |
--zeno-spacing-7 | 1.75rem | |
--zeno-spacing-8 | 2rem | |
--zeno-spacing-9 | 2.25rem | |
--zeno-spacing-10 | 2.5rem | |
--zeno-spacing-11 | 2.75rem | |
--zeno-spacing-12 | 3rem | |
--zeno-spacing-16 | 4rem | |
--zeno-spacing-20 | 5rem | |
--zeno-spacing-24 | 6rem | |
--zeno-spacing-32 | 8rem |
Radius
| Token | Value | Sample |
|---|---|---|
--zeno-radius-none | 0 | |
--zeno-radius-xs | 2px | |
--zeno-radius-sm | 4px | |
--zeno-radius-md | 6px | |
--zeno-radius-lg | 8px | |
--zeno-radius-xl | 12px | |
--zeno-radius-2xl | 16px | |
--zeno-radius-3xl | 24px | |
--zeno-radius-full | 9999px |
Shadow
Shadows are stronger in the dark theme, where a soft shadow would not show.
| Token | Light | Dark |
|---|---|---|
--zeno-shadow-none | none | none |
--zeno-shadow-xs | 0 1px 2px 0 rgba(16, 18, 21, 0.06) | 0 1px 2px 0 rgba(0, 0, 0, 0.4) |
--zeno-shadow-sm | 0 1px 3px 0 rgba(16, 18, 21, 0.1), 0 1px 2px -1px rgba(16, 18, 21, 0.08) | 0 1px 3px 0 rgba(0, 0, 0, 0.5), 0 1px 2px -1px rgba(0, 0, 0, 0.4) |
--zeno-shadow-md | 0 4px 8px -2px rgba(16, 18, 21, 0.1), 0 2px 4px -2px rgba(16, 18, 21, 0.06) | 0 4px 8px -2px rgba(0, 0, 0, 0.5), 0 2px 4px -2px rgba(0, 0, 0, 0.4) |
--zeno-shadow-lg | 0 12px 20px -4px rgba(16, 18, 21, 0.12), 0 4px 8px -4px rgba(16, 18, 21, 0.06) | 0 12px 20px -4px rgba(0, 0, 0, 0.55), 0 4px 8px -4px rgba(0, 0, 0, 0.4) |
--zeno-shadow-xl | 0 20px 32px -8px rgba(16, 18, 21, 0.16), 0 8px 12px -6px rgba(16, 18, 21, 0.06) | 0 20px 32px -8px rgba(0, 0, 0, 0.6), 0 8px 12px -6px rgba(0, 0, 0, 0.4) |
--zeno-shadow-2xl | 0 28px 56px -12px rgba(16, 18, 21, 0.24) | 0 28px 56px -12px rgba(0, 0, 0, 0.7) |
--zeno-shadow-inset | inset 0 1px 2px 0 rgba(16, 18, 21, 0.08) | inset 0 1px 2px 0 rgba(0, 0, 0, 0.4) |
Motion
Press Play to move a marker along each row with that row's token. If your system asks for reduced motion, the markers jump without moving.
Durations
| Token | Value | Sample |
|---|---|---|
--zeno-motion-duration-instant | 0ms | |
--zeno-motion-duration-xs | 75ms | |
--zeno-motion-duration-sm | 150ms | |
--zeno-motion-duration-md | 200ms | |
--zeno-motion-duration-lg | 300ms | |
--zeno-motion-duration-xl | 500ms | |
--zeno-motion-duration-2xl | 700ms |
Easings
| Token | Value | Sample |
|---|---|---|
--zeno-motion-easing-linear | cubic-bezier(0, 0, 1, 1) | |
--zeno-motion-easing-ease | cubic-bezier(0.25, 0.1, 0.25, 1) | |
--zeno-motion-easing-ease-in | cubic-bezier(0.42, 0, 1, 1) | |
--zeno-motion-easing-ease-out | cubic-bezier(0, 0, 0.58, 1) | |
--zeno-motion-easing-ease-in-out | cubic-bezier(0.42, 0, 0.58, 1) | |
--zeno-motion-easing-standard | cubic-bezier(0.2, 0, 0, 1) | |
--zeno-motion-easing-spring | cubic-bezier(0.175, 0.885, 0.32, 1.275) | |
--zeno-motion-easing-bounce | cubic-bezier(0.68, -0.55, 0.265, 1.55) |
Sizes
Controls
| Token | Value | Sample |
|---|---|---|
--zeno-size-control-xs | 20px | |
--zeno-size-control-sm | 24px | |
--zeno-size-control-md | 32px | |
--zeno-size-control-lg | 40px | |
--zeno-size-control-xl | 48px | |
--zeno-size-control-2xl | 56px |
Icons
| Token | Value | Sample |
|---|---|---|
--zeno-size-icon-xs | 12px | |
--zeno-size-icon-sm | 16px | |
--zeno-size-icon-md | 20px | |
--zeno-size-icon-lg | 24px | |
--zeno-size-icon-xl | 32px | |
--zeno-size-icon-2xl | 48px |
Avatars
| Token | Value | Sample |
|---|---|---|
--zeno-size-avatar-xs | 24px | |
--zeno-size-avatar-sm | 32px | |
--zeno-size-avatar-md | 40px | |
--zeno-size-avatar-lg | 48px | |
--zeno-size-avatar-xl | 64px | |
--zeno-size-avatar-2xl | 96px |
Smallest target
| Token | Value | Sample |
|---|---|---|
--zeno-size-target-min | 24px |
Borders and the focus ring
| Token | Value | Sample |
|---|---|---|
--zeno-border-width-thin | 1px | |
--zeno-border-width-medium | 2px | |
--zeno-border-width-thick | 4px |
| Token | Value |
|---|---|
--zeno-focus-ring-width | 2px |
--zeno-focus-ring-offset | 2px |
Opacity
| Token | Value | Sample |
|---|---|---|
--zeno-opacity-disabled | 0.5 | |
--zeno-opacity-loading | 0.7 |
Breakpoints
CSS custom properties cannot be used in a media query. These are for JavaScript and for reference.
| Token | Value |
|---|---|
--zeno-breakpoint-xs | 480px |
--zeno-breakpoint-sm | 640px |
--zeno-breakpoint-md | 768px |
--zeno-breakpoint-lg | 1024px |
--zeno-breakpoint-xl | 1280px |
--zeno-breakpoint-2xl | 1536px |
Layers
| Token | Value |
|---|---|
--zeno-z-index-base | 0 |
--zeno-z-index-raised | 1 |
--zeno-z-index-dropdown | 1000 |
--zeno-z-index-sticky | 1100 |
--zeno-z-index-drawer | 1200 |
--zeno-z-index-modal | 1300 |
--zeno-z-index-popover | 1400 |
--zeno-z-index-toast | 1500 |
--zeno-z-index-tooltip | 1600 |
Use tokens in JavaScript
import { tokens, light, dark, vars } from '@usezeno/tokens';
tokens.spacing[4]; // '1rem'
light.color.text.primary; // the resolved light value
vars.color.text.primary; // 'var(--zeno-color-text-primary)'