ReferenceColor scales tokens
Roadie's ThemeProvider turns one hex accent colour into 14-step OKLCH scales
at runtime and handles dark mode at the same time. RoadieProvider mounts it
at the root, and its theme prop takes the provider's options. Leave the
accent alone for apps with a single brand colour, control it for apps themed
from data, or add the pre-hydration bootstrap helpers for static exports that
need cold loads with no flash. The Colors page covers
the surface tokens the accent flows into.
--accent-hue and --accent-chroma, which Roadie's CSS feeds into
oklch() curves. Changing the accent updates every component that reads the
accent or neutral scale through the cascade. There are no component-level
theme props..dark class on <html>, which swaps a second set of
OKLCH values. ThemeProvider handles the toggle, localStorage
persistence, and optionally following prefers-color-scheme. Dark mode and
accent colour are independent, and setting one never resets the other.intent-*
utility classes and flows down through CSS custom properties. Child
components inherit it, with no context provider per intent.--chart-highlight follows the accent. The math
happens when CSS resolves, not in JavaScript, so the scales stay consistent
across reflows.On the docs site, DEFAULT_ACCENT_COLOR and the CSS defaults of the two
accent properties are listed, followed by every colour scale that reads
--accent-hue, live in the current theme. Pick an accent in the
appearance settings and they change with it.
DEFAULT_ACCENT_COLOR--accent-hue--accent-chromaNeutral
Accent
For apps with a single brand colour that never changes at runtime, pass
defaultAccentColor in RoadieProvider's theme. Children can still call
useTheme().setAccentColor(hex), such as from an in-app colour picker, and
the provider's own state tracks the change.
When the accent colour comes from async data, such as a CMS field, a feature
flag, or per-tenant config, pass it as accentColor. The provider becomes
controlled: the prop wins on every render, and setAccentColor calls do
nothing but log a dev warning. Pass null to stay controlled while falling
back to defaultAccentColor. Without a defaultAccentColor, null falls back
to Oztix blue in the root provider and to the parent's accent in a nested one.
The provider follows the prop whenever it changes, with no useEffect,
cleanup, or reset logic. Passing null while the query loads falls back to
defaultAccentColor, so the theme never renders in a broken state.
A nested provider has no default of its own: with null, no accent, or an
invalid one, it inherits the accent around it, whether the root's or an
enclosing nested provider's, and so do popups that open from inside it. It
sets no scope until its accent arrives, and its children don't remount when it
does.
Roadie validates hex input at two points:
setAccentColor(hex) throws an InvalidColorError straight away when the
argument isn't a valid hex colour. Wrap the call in a try and catch, or
validate first with isValidHexColor.accentColor prop logs a dev warning, and the provider renders
with defaultAccentColor, or the parent's accent when nested, so the app
never shows a broken theme.React hydrates after the first paint, so an app that only sets the accent
inside ThemeProvider flashes the default blue on a cold load until it
hydrates. For apps that know the accent colour on the server, such as
per-tenant branding, promoter-branded pages, or statically generated routes,
use the synchronous bootstrap helpers to set --accent-hue and
--accent-chroma before the first paint.
For React frameworks, such as Next.js or Remix, inject the theme script and
the accent style as separate head children. getAccentStyleSync returns only
the inner CSS, so you can wrap it in a real <style> element. Give the root
provider the same accent. It keeps a style that already holds its accent, and
replaces one that holds another accent once it hydrates.
For Astro, Nuxt, or plain HTML, use getBootstrapScript. It returns one HTML
string with both the theme script and the accent style tag, ready to drop
into <head>.
Both paths use the same synchronous sRGB to OKLCH converter, which matches
colorjs.io's lightness and chroma to four decimal places. Modern browsers
(Chrome 111+, Safari 15.4+, and Firefox 113+) render the accent scale from
oklch() math directly, so the two custom properties are enough for a cold
load with no flash. Older browsers fall back to the compiled CSS accent until
the client-side accent effect runs.
@oztix/roadie-core/theme exports DEFAULT_ACCENT_COLOR, the accent step 9
that the tokens ship, so consumers never hardcode the default hex.
@oztix/roadie-components re-exports it. Reset to it three ways:
accentColor={someHex ?? null}, and
Roadie turns null back into the default. A nested provider turns null
into its parent's accent, so pass DEFAULT_ACCENT_COLOR to reset one.setAccentColor(DEFAULT_ACCENT_COLOR)
from a reset button.getBootstrapScript, omit accentColor or pass null. The default CSS
accent applies, and the bootstrap emits no style tag.Accent colour and dark mode are independent but share one provider.
getThemeScript prevents a flash of the wrong theme on the first load by
reading localStorage before the first paint, and getBootstrapScript adds
the accent style tag to it for one head injection.
followSystem follows prefers-color-scheme until the user toggles.defaultDark sets the dark state when no preference is stored.useTheme().setDark(boolean) persists an explicit choice to
localStorage.useTheme().isDark is the current state, and it updates as it changes.Fetch the accent, render the provider with accentColor, and let loading
states flow through null, with no effects or cleanup. Inside
RoadieProvider, a nested provider like this one themes only its own
subtree, as per-route theming
describes.
A route's layout.tsx can fetch its own data and wrap its children in a
nested ThemeProvider. A nested provider sets the accent on a wrapper around
its children, so the route's accent themes only the route, and the root's
accent stays on everything outside it. When the route closes, its wrapper goes
with it. Menus, dialogs, and other popups that open from the route carry its
accent too. The wrapper uses display: contents, so it doesn't change the
layout, but a parent's selectors for its direct children, such as *:
variants, see the wrapper instead of the route's elements. Inside a list or
table, put the provider inside an item or cell, since the wrapper is a div.
It renders on the server, so the route's first paint already has its accent.
Like the root, it starts its subtree at the neutral intent. Browsers without
OKLCH keep the root's accent inside the route. When a route has no
themeColour, the provider gets null, and the route keeps the root's accent
and the intent around it.
For output: 'export' apps, wrap the page in a ThemeProvider, nested
inside the root one, with the accent you fetch at build time. The wrapper and its accent are in the
exported HTML, so the first paint already uses the themed colours, and
hydration has nothing to rewrite. For an accent that themes the whole
document, use the head injection helpers and give the
root provider the same accent instead.
DEFAULT_ACCENT_COLOR is the Oztix blue default, from
@oztix/roadie-core/theme.isValidHexColor(input) is a synchronous type guard for validating at the
fetch boundary.InvalidColorError is thrown by setAccentColor, getAccentStyleSync,
getAccentStyleTagSync, and getAccentStyleTag on invalid input.
getBootstrapScript throws a plain Error.getThemeScript(options) returns the inline script body that prevents a
dark mode flash.getAccentStyleSync(hex) returns the inner CSS, for React.getAccentStyleTagSync(hex) returns a full <style> tag, for any
framework.getAccentStyleTag(hex) is the async variant, with full hex fallbacks for
browsers without OKLCH.getBootstrapScript(options) combines the theme script and the accent
style tag for one head injection.Reads and updates the theme mounted by the nearest ThemeProvider, including
the one RoadieProvider mounts. Throws outside one.
| Field | Type | Description |
|---|---|---|
accentColor | string | The current accent, as a hex string. |
setAccentColor | (color: string) => void | Sets the accent. Throws InvalidColorError on a non-hex value. |
isDark | boolean | Whether dark mode is active. |
setDark | (dark: boolean) => void | Sets dark mode and persists the choice to localStorage. |
Controlled accent colour. When provided (including `null`), the provider operates in controlled mode: this value overrides internal state on every render and imperative `setAccentColor` calls become no-ops with a dev warning. Pass `null` to opt into controlled mode while falling back to `defaultAccentColor` (useful for async data: `collection?.themeColour ?? null`). Without a `defaultAccentColor`, `null` falls back to the parent provider's accent when nested, or to Oztix blue at the root.
Initial accent colour when uncontrolled, and the fallback for a `null` or invalid `accentColor`. Defaults to the parent provider's accent when nested, or to Oztix blue at the root.
Defaults to #0191eb.
Initial dark mode state when no stored preference exists (default: false)
Defaults to false.
Respect prefers-color-scheme when no explicit user choice is stored (default: false)
Defaults to false.