Foundations
Colors
Colors are roles, not swatches. Each role is a tone of a tonal palette generated (in HCT) from a seed color, so contrast is arithmetic: a tone gap of 50 guarantees 4.5:1. Every role is a light-dark() value, so it follows data-theme at any depth with no separate dark stylesheet. A build-time test checks every role pair against WCAG 2.2 AA in light, dark, medium and high contrast, for every brand. React Native gets the same roles, resolved to hex per scheme and contrast level, from @nexub/tokens/native.
background: var(--color-surface-container-low);
color: var(--color-on-surface);
/* Tailwind: bg-surface-container-low text-on-surface */Surfaces
In-flow content steps through the container tiers; floating layers (menus, dialogs) use surface-elevated, which is lighter than the page in dark mode.
Accents
Each accent comes as four roles: the color, its on- text, a quieter -container, and on-…-container. Brands (data-product) remap tertiary.
Text and outlines
--color-on-surface--color-on-surface-variant--color-on-surface-muted--color-primary--color-outline (≥ 3:1, field borders, focus)--color-outline-variant (hairlines, dividers)Contrast levels
Three levels: standard (the default), medium and high. Set data-contrast on <html> or on any subtree; without it, prefers-contrast: more selects high. Accents, secondary text and outlines move further from their surfaces; surfaces stay put. Medium takes the tone halfway between the other two, for users who want more separation without the starkness of high.
<html data-contrast="medium"> <!-- standard | medium | high -->Data visualisation
Chart palettes are roles too: light-dark aware, generated in HCT. Use --color-chart-* (Tailwind: fill-chart-1, bg-chart-seq-4, stroke-chart-div-7).
Categorical
Eight series for unordered categories. Each clears 3:1 against surface and surface-container in both schemes (WCAG 1.4.11), and a build-time test simulates protanopia, deuteranopia and tritanopia to keep the first six apart. Past six series, label directly or group the rest as “Other”; never rely on colour alone.
Sequential
Seven steps of primary for ordered values (low → high). Step 1 is closest to the surface in either scheme.
Diverging
Error → neutral → primary, for values either side of a midpoint (step 4). Swap the ends if “negative” is not bad news in your data.
Wide gamut
On P3 displays (color-gamut: p3, with relative colour support) the accent fills — primary, tertiary, the product accents, success, warning, error and info — gain 18% OKLCH chroma. Lightness is unchanged, so every contrast pair holds. Containers, text and surfaces stay in sRGB. Nothing to opt into.
Tonal palettes
The raw tones behind the roles, --color-palette-<name>-<tone>. Prefer roles; reach for a tone only for illustration or data visualisation.
primarysecondaryneutralneutral-varianterrorsuccesswarningleapbookubpayubfounderubBrand theming from a seed colour
For white-label products, pass one hex colour and the accent roles are regenerated from it with the same HCT tone table as the built-in themes, so every pair still meets WCAG AA in light, dark and every contrast level. Like Material 3, the seed is a starting hue, not a fixed swatch: a light yellow becomes a readable olive.
<ThemeProvider seed="#e4572e">…</ThemeProvider>
// a product brand instead of the primary accent:
<ThemeProvider seed="#e4572e" seedTarget="tertiary">…</ThemeProvider>ThemeProvider loads the colour maths only when seed is set and applies the roles through the CSSOM, so it works under a strict CSP. Because that happens after hydration, server-rendered apps should also ship the static CSS to avoid a flash: createSeedThemeCss('#e4572e') from @nexub/tokens/seed. Seeding covers the primary (or tertiary) family; secondary, the chart palettes and P3 wide-gamut boosts stay on the Nexub brand.
Legacy names
--color-bg-*, --color-text-*, --color-border-*, --color-semantic-* and --color-fill-* still work. They alias the roles (e.g. --color-text-primary → --color-on-surface). New code should use the roles.