Theming overview
oks-ui has no theme provider, no config object, and no runtime JavaScript for styling. Every visual decision is a CSS custom property you can override from your own stylesheet.
Workspace settings
Changes save to every member.
:root {
/* nothing overridden yet */
}This is oks-ui with no overrides.
The three token layers
Every colour resolves through three layers, from raw to semantic. You can override at any layer.
- Palette —
--oks-palette-<name>-<shade>. The raw ramps (slate,blue,violet, …), each shade50…950, each with a light and a dark value. - Role —
--oks-color-<role>-<shade>. The semantic roles (primary,secondary,success,warning,danger,info,neutral,default). Each role is an alias onto a palette. - Component — e.g.
--oks-button-tone-text,--oks-chip-bg. Each component reads a role by default but exposes its own hooks for one-off overrides (see below).
How a solid primary Button resolves, end to end:
--oks-button-tone-text /* the fill of a solid Button */
→ var(--oks-color-primary-700) /* role alias */
→ var(--oks-palette-primary-700) /* palette shade */
→ #b44801 /* the value, in light mode */Override any link in that chain and everything downstream follows — with no rebuild, because the components already reference the variables at runtime.
Override a token
Set the custom property on :root from your own stylesheet, imported after oks-ui's. That's the whole API.
:root {
--oks-color-primary-500: #4f46e5;
--oks-radius-md: 10px;
--oks-space-4: 18px;
}Override the whole ramp, not just 500
This is the one that trips people up. Different variants of the same component read different shades of a role. If you override only 500, your solid buttons recolour but your soft chips, tints, and hover states keep the old hue.
| Shade | Used for |
|---|---|
| 50 / 100 | Soft/tinted backgrounds and borders (soft Chip, soft Button, subtle fills) |
| 500 | Solid fills, the resting brand colour, focus rings |
| 600 | Hover state of solid fills |
| 700 | Active state, and text on a soft/tinted background |
So a complete brand override sets the ramp — at minimum 50, 100, 500, 600, 700; ideally all of 50…950:
:root {
--oks-color-primary-50: #eef2ff;
--oks-color-primary-100: #e0e7ff;
--oks-color-primary-200: #c7d2fe;
--oks-color-primary-300: #a5b4fc;
--oks-color-primary-400: #818cf8;
--oks-color-primary-500: #6366f1; /* brand */
--oks-color-primary-600: #4f46e5; /* hover */
--oks-color-primary-700: #4338ca; /* active / soft text */
--oks-color-primary-800: #3730a3;
--oks-color-primary-900: #312e81;
--oks-color-primary-950: #1e1b4b;
}Point a role at a different palette
Roles are just aliases, so you can re-point one at any built-in palette (there are 22, the full Tailwind set) without touching a single shade. Every color="secondary" prop follows.
:root {
/* make "secondary" the slate ramp, "info" the sky ramp */
--oks-color-secondary-500: var(--oks-palette-slate-500);
--oks-color-secondary-600: var(--oks-palette-slate-600);
--oks-color-info-500: var(--oks-palette-sky-500);
}Surfaces & borders
Two tokens drive the neutral chrome that sits behind everything — cards, tables, popovers, inputs:
--oks-color-surface— the raised panel background.--oks-color-border— the shared hairline. Repoint it once and every card, table, nav, and accordion border changes together.
--oks-color-surface and the --oks-form-field-* tokens ship a light value only. If you build a dark theme by hand (rather than using the built-in data-theme="dark" switch), supply dark values yourself. See Dark mode.Radius & density
Shape and spacing are tokens too. The radius scale (--oks-radius-sm|md|lg|xl|2xl|full) sets how round everything is; the spacing scale (--oks-space-0…96) drives internal padding and gaps. Nudge them for a tighter or softer feel:
:root {
--oks-radius-md: 2px; /* sharp, technical */
--oks-radius-lg: 4px;
}
/* or go fully rounded */
:root {
--oks-radius-md: 12px;
--oks-radius-lg: 20px;
}Per-component hooks
When you want to change one component in one place — not the global token — every component exposes its own custom properties. Set the colour ones with the component's style prop: components that compute their colours set those variables on themselves, so a class or a wrapper cannot override them (radius, height and the form-field variables can be set from anywhere). A few of the most useful (the plain --oks-button-tone and --oks-chip-tone only tint dots and decorative parts; recolour a fill or text with the -tone-text variables):
--oks-button-tone-text(the fill of a solid button and the text of the others), with-hoverand-activevariants;--oks-button-radius,--oks-button-height--oks-chip-tone-text(text, and the fill of a solid chip),--oks-chip-bg--oks-form-field-bg,--oks-form-field-border,--oks-form-field-label
{/* one ink-coloured button, without touching the primary role */}
<Button
color="primary"
style={{
"--oks-button-tone-text": "var(--oks-palette-neutral-900)",
"--oks-button-tone-text-hover": "var(--oks-palette-neutral-800)",
"--oks-button-tone-text-active": "var(--oks-palette-neutral-950)",
} as React.CSSProperties}
>
Sign in
</Button>Theme through props
Not everything needs CSS. Two props cover most per-instance colour choices:
color— one of the roles:default,primary,secondary,info,success,warningordanger. To use a palette the roles don't map to, point a role at it (for example every--oks-color-secondary-*shade at--oks-palette-teal-*).colorDepth— which shade of that colour to use (default500).colorDepth={600}for a deeper tone.
<Chip color="primary">Default depth</Chip>
<Chip color="primary" colorDepth={600}>Deeper</Chip>
<Button color="danger" variant="soft">Delete</Button>The prefixed utility layer
oks-ui/utilities.css is an opt-in set of .oks-* classes for spacing and radius that read the same tokens — .oks-p-4, .oks-mt-2, .oks-rounded-lg. It has no colour utilities: colour comes from the component props and the token variables. Import it if you want quick token-aware spacing; skip it if you already have Tailwind or your own.
Why overrides always win
oks-ui wraps its token rules in :where(), which drops them to zero specificity. A bare :root override in your own CSS therefore always wins — in light and dark mode — without !important and without matching selector weight.