ReferenceColor scales tokensIntents tokensData visualisation tokensEmphasis and states tokens
Roadie uses OKLCH colour scales with 14 steps, 0 to 13. Colours are applied
through three utility types, bg-*, text-*, and border-*, and each is
scoped to its own CSS property.
Each scale has 14 OKLCH steps. Step 0 is the lightest extreme and step 13 the darkest. Dark mode swaps the values, and the step numbers stay the same.
On the docs site, each scale is a row of swatches in the current theme, with each step's number. Color scales lists every step's value in light and dark.
Neutral
Brand
Brand secondary
Accent
Danger
Success
Warning
Info
Three utility namespaces apply colour. Each maps a semantic level to a scale step of the current intent.
| Level | bg-* | text-* | border-* |
|---|---|---|---|
| subtler | Step 9 at 6% alpha (dark: 6 to 10%, by intent) | Step 10 | Step 5 |
| subtle | Step 9 at 11% alpha (dark: 11 to 18%, by intent) | Step 11 | Step 6 (dark neutral: 7) |
| normal | Step 1 (dark: 2) | Step 12 | Step 7 (dark neutral: 8) |
| strong | Step 9 (neutral: 13, danger: 10; success and brand-secondary mix 10% toward white) | Step 13 | Step 9 (dark neutral: 10) |
| inverted | Step 12 | Step 0, the page's text flipped. It pairs with bg-inverted. | Step 12 |
| on-strong | None | White or step 13, chosen per intent. It's the label colour for strong fills. | None |
| raised | Step 1 (dark: 4 for neutral, 2 for the other intents) | None | None |
| sunken | Step 2 (dark: 0) | None | None |
| mark | Light step 5, kept in dark mode (neutral: step 13) | Light step 13, kept in dark mode (neutral: step 0) | None |
For example, bg-normal, bg-subtle, bg-raised, text-subtle,
text-strong, border-subtle, border-b-subtler, and divide-subtler.
Four depths in one picture. Pick the surface that matches the element's depth in the layout: sunken for recessed areas, normal for the page, raised for elevated cards, and floating for popovers and modals.
| Surface | Use for |
|---|---|
| Sunken | bg-sunken, for recessed areas, inset panels, and empty-state wells. emphasis-sunken adds the inset shadow. |
| Normal | bg-normal, the default page background and the baseline everything else sits on. Use it when nothing should feel elevated or recessed. |
| Raised | bg-raised, for elevated cards, sticky headers, and grouped content that should lift off the page. emphasis-raised adds the rim light and shadow-md. |
| Floating | emphasis-floating, for popovers, modals, dropdowns, and anything else that floats over the document flow. It uses the raised background, lifted in dark mode, shadow-xl, and the strong rim light. Add is-translucent to let content show through as it scrolls beneath. |
The four levels go from sunken to normal, raised, and floating. Reach
for the emphasis-* shortcut when you need the background, text, and shadow
together, and use bg-raised or bg-sunken directly when you only need the
background colour.
Set intent-* on a container to choose the palette. Use the emphasis-*
shortcuts for background, text, and border together, with hover and press
states once you add is-interactive, or the individual utilities when you
need to compose them.
On the docs site, each intent shows the emphasis-strong, emphasis-normal,
emphasis-subtle, and emphasis-subtler presets.
intent-neutralintent-brandintent-brand-secondaryintent-brand-purpleintent-brand-blueintent-brand-orangeintent-accentintent-dangerintent-successintent-warningintent-infoBrand intents are named by hue. brand-blue is the same as brand, and
brand-orange is the same as brand-secondary. brand-purple uses the
purple scale that info also uses. Green, yellow, and red stay status
colours, with no brand intent.
The scales have hue names too, as variables only. --color-brand-blue-{n}
is --color-brand-{n}, --color-brand-purple-{n} is --color-info-{n}, and
--color-brand-orange-{n} is --color-brand-secondary-{n}. Steps 0 to 13
follow dark mode with the scale they name, and the fixed light steps, such as
--color-brand-orange-light-9, stay light.
Roadie measures all contrast with APCA, the contrast method in the WCAG 3 draft, and doesn't use WCAG 2 ratios. APCA scores a pair as Lc, its lightness contrast, and weighs text size and weight in a way ratios don't.
| Content | Minimum |
|---|---|
| Body text | Lc 75 |
| Labels on strong fills and large text | Lc 60 |
| Non-text UI and large display text | Lc 45 |
text-on-strong is the label colour for strong fills, chosen per intent:
white or dark text, whichever reads better on the fill. emphasis-strong uses
it. Labels need Lc 60 or more at rest, on hover, and when pressed.
text-inverted is different: it's the page's text colour flipped, and pairs
with bg-inverted.
White on Spotlight blue scores Lc 66, so blue fills keep white labels. Danger, success, and brand-secondary move their fill slightly to reach Lc 60.
A custom accent keeps white text too. ThemeProvider caps its chroma to what
sRGB can show at step 9, because a browser clips a more saturated fill and the
clip makes it lighter.
| Intent | Light | Dark | APCA Lc, light |
|---|---|---|---|
| neutral | White | Step 0 | 107 |
| brand | White | White | 66 |
| brand-secondary | Step 13 | Step 0 | 61 |
| brand-blue | White | White | 66 |
| brand-orange | Step 13 | Step 0 | 61 |
| brand-purple | White | White | 68 |
| accent | White | White | 66 |
| danger | White | White | 64 |
| success | Step 13 | Step 0 | 62 |
| warning | Step 13 | Step 0 | 62 |
| info | White | White | 68 |
Charts use their own palette, checked for colour-blind separation in CI. Each set has one job. Read Data visualisation before you pick one. On the docs site, each set shows its swatches on a light and a dark chart surface.
For telling series apart.
Light
1
2
3
4
5
6
7
8
Dark
1
2
3
4
5
6
7
8
For how much.
Light
0
1
2
3
4
5
6
7
8
Dark
0
1
2
3
4
5
6
7
8
For ahead of or behind a benchmark.
Light
pos-4
pos-3
pos-2
pos-1
0
neg-1
neg-2
neg-3
neg-4
Dark
pos-4
pos-3
pos-2
pos-1
0
neg-1
neg-2
neg-3
neg-4
For meaning only.
Light
good
warning
serious
critical
Dark
good
warning
serious
critical
Dark mode is automatic. The .dark class on <html> swaps every OKLCH value,
so no dark: Tailwind variants are needed. bg-normal, text-subtle, and
border-subtle all adapt by themselves.
Dark steps sit closer together and shadows barely show on dark, so dark mode leans on fills and edges instead:
border-subtle, border-normal, and border-strong take one step
higher, to keep their contrast against dark surfaces.--intent-bg-floating), and a raised surface inside a floating one lifts
20% again, so each layer stands off what's under it.text-subtle moves 40% toward step 12, so
it keeps Lc 75 on the lighter fills. An intent set inside one moves too.emphasis-subtle track with an emphasis-raised pill in it, as in
Tabs, takes the sunken fill, since a tint lifts it toward the pill on a dark
page. The pill then stands off it on any surface.emphasis-field stays sunken, as in light mode, with its edge at 50% of
step 9 instead of 22%, so the field's outline shows on the page.The CSS also sets color-scheme: light on :root and color-scheme: dark on
.dark, so native browser UI, such as scrollbars and form controls, matches
your theme.
Brand artwork such as patterns, illustrations, and print keeps its light
colours in dark mode. Use --color-{scale}-light-{0|5|9|12|13} for these.
.dark doesn't override them, and only neutral has step 0.
Add a blocking script in <head> to prevent a flash of the wrong theme. The
getThemeScript helper generates it for you. Import it from
@oztix/roadie-core/theme, which has no React dependency.
import { getThemeScript } from '@oztix/roadie-core/theme'// In your <head>:<meta name="color-scheme" content="light" /><script dangerouslySetInnerHTML={{ __html: getThemeScript() }} />
By default, the theme is light. Pass followSystem: true to respect the
user's OS preference when they haven't explicitly toggled.
getThemeScript({ followSystem: true })
For React apps, wrap your app in ThemeProvider to get the useTheme() hook
with isDark and setDark.
Once a user explicitly toggles, their choice persists in localStorage and
overrides the system preference.
For apps without React, use getThemeScript() for the initial load and toggle
with plain DOM calls.
function setDark(dark) {document.documentElement.classList.toggle('dark', dark)document.documentElement.style.colorScheme = dark ? 'dark' : 'light'localStorage.setItem('theme', dark ? 'dark' : 'light')}
The accent scale is CSS-native. ThemeProvider sets --accent-hue and
--accent-chroma, and CSS oklch() computes every accent and neutral colour
in the browser. Modern browsers need no JavaScript colour generation.