Skip to content
nexubDesign System
v2.4.0

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.

dim
--color-surface-dim
surface
--color-surface
bright
--color-surface-bright
container lowest
--color-surface-container-lowest
container low
--color-surface-container-low
container
--color-surface-container
container high
--color-surface-container-high
container highest
--color-surface-container-highest
elevated
--color-surface-elevated
inverse surface
--color-inverse-surface

Accents

Each accent comes as four roles: the color, its on- text, a quieter -container, and on-…-container. Brands (data-product) remap tertiary.

primary
--color-primary
primary container
--color-primary-container
secondary
--color-secondary
secondary container
--color-secondary-container
tertiary
--color-tertiary
tertiary container
--color-tertiary-container
error
--color-error
error container
--color-error-container
success
--color-success
success container
--color-success-container
warning
--color-warning
warning container
--color-warning-container
info
--color-info
info container
--color-info-container

Text and outlines

Primary text--color-on-surface
Secondary text--color-on-surface-variant
Tertiary text (still ≥ 4.5:1 on every container)--color-on-surface-muted
Links--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 -->
primary (standard)
--color-primary
variant text
--color-surface-container
primary (medium)
--color-primary
variant text
--color-surface-container
primary (high)
--color-primary
variant text
--color-surface-container

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.

1
2
3
4
5
6
7
8

Sequential

Seven steps of primary for ordered values (low → high). Step 1 is closest to the surface in either scheme.

seq-1
seq-2
seq-3
seq-4
seq-5
seq-6
seq-7

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.

div-1
div-2
div-3
div-4
div-5
div-6
div-7

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.

primary
secondary
neutral
neutral-variant
error
success
warning
leap
bookub
payub
founderub

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