Skip to content

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.

LiveOverview

Workspace settings

Changes save to every member.

Pro plan
: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.

  1. Palette — --oks-palette-<name>-<shade>. The raw ramps (slate, blue, violet, …), each shade 50…950, each with a light and a dark value.
  2. Role — --oks-color-<role>-<shade>. The semantic roles (primary, secondary, success, warning, danger, info, neutral, default). Each role is an alias onto a palette.
  3. 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.

ShadeUsed for
50 / 100Soft/tinted backgrounds and borders (soft Chip, soft Button, subtle fills)
500Solid fills, the resting brand colour, focus rings
600Hover state of solid fills
700Active 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.

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 -hover and -active variants; --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, warning or danger. 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 (default 500). 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.