Redpanda UIRedpanda UI
Foundation

Theming your app

Restyle the whole system from one override sheet, and keep it honest as the registry moves.

Every colour, radius and shadow a component paints comes from a token in theme.css, so restyling the system is a file of your own that redeclares the tokens you want to move. Components are untouched — which is what lets you take the next registry update.

The shape of it

@redpanda/theme installs an example sheet next to the theme. Copy it out first: everything under redpanda-ui/ is ours and gets replaced by the next shadcn add, and this file is yours.

cp components/redpanda-ui/style/theme-overrides.example.css src/theme-overrides.css

Import it after the theme — @theme blocks merge last-wins, and a [data-theme='dark'] block outranks the registry's by source order:

src/globals.css
@import "tailwindcss";
@import "tw-animate-css";
@import "./components/redpanda-ui/style/theme.css";
@import "./theme-overrides.css";

A sheet has two halves, and they are not symmetrical:

src/theme-overrides.css
@theme {
  /* The one shape knob — every rounded-* rung derives from it. Not per-theme. */
  --radius: 0.25rem;

  --color-primary: #c93617;
  --color-primary-hover: #b1300f;
  --color-primary-pressed: #9c2a0d;
  --color-primary-foreground: #fff4f0;
  --color-primary-wash: rgba(201, 54, 23, 0.07);
  --color-primary-wash-pressed: rgba(201, 54, 23, 0.12);
}

/* Add `.dark` beside this selector if something in your app sets the class instead — the shipped
   theme answers to both, and a sheet that answers to one of them goes dark-blind in the other. */
[data-theme='dark'] {
  --color-primary: #ff7a52;
  --color-primary-hover: #ff9575;
  --color-primary-pressed: #ffb096;
  --color-primary-foreground: #101010;
  --color-primary-wash: rgba(255, 122, 82, 0.09);
  --color-primary-wash-pressed: rgba(255, 122, 82, 0.15);
}

You need no ramp behind those values and there is none to reach for: each token holds its own literal, so theme.css is the complete list of colours the system has. Pick roles with the three questions on the Theme page — what is it made of, how does it react, what sits on it.

Four rules, and why each one is invisible without a checker

A colour token that names nothing compiles to nothing. There is no error and no warning: the class still exists, it just paints our value where you meant yours. That is the failure mode every rule here guards, which is why the theme ships a checker rather than a style guide.

  1. Name only tokens the theme declares. A role renamed or dropped in a registry update leaves your sheet quietly ineffective for that role.
  2. A family you override the rest of, you override the states of. Half a family is the worst case, not a partial one: the control takes your colour at rest and ours under the pointer — the one state no screenshot at rest can show. That includes the -wash resting beside a tone, and surface-recess, which is surface-subtle at half alpha rather than a role of its own.
  3. Override both themes, or neither. The registry's dark block still applies to every rung you leave alone, so a half-converted sheet puts our indigo under the pointer inside your palette.
  4. static-* has no dark value, by definition — it is the ground that stays dark in both themes. Declare it in the light block only, and always with its -foreground: the ink does not flip either, so moving the ground alone is how light ink ends up on a light fill in exactly one theme.

Wire the checker into your build

package.json
{
  "scripts": {
    "theme:check": "bun components/redpanda-ui/style/theme-check/cli.mts --overrides src/theme-overrides.css",
    "build": "npm run theme:check && next build"
  }
}
src/theme-overrides.css ok — 281 semantic overrides across 145 roles, 15 registry roles
deliberately inherited, no dead names.

It reports six things, each needing your sheet and theme.css read together: a dead name the theme does not declare, a duplicate that makes an earlier line unreachable, an unresolved var() chain, a flipped static-* role in the dark block, an incomplete static ground with no ink, and a hole — half a family.

flagdefault
--overridesrequired — the sheet to check
--theme../theme.css beside the script, which is where @redpanda/theme installs it
--palettecomma-separated scale prefixes your sheet owns outright (ink,ember), exempt from rule 1

--palette is for a sheet that keeps its own ramps: define --color-ink-* and point the semantic tokens at them with var() if that is how your design system is organised. The registry has no ramps of its own, deliberately — a rung a call site pins itself to is a rung that can never move — but your sheet is free to.

It is the same code the registry runs on its own consumers: the rule that decides what counts as half a family is one module (theme-check/rule.mts), called by this CLI, by the checker on Lookout's sheet, and by the test behind the playground's built-in themes. A consumer's checker and ours cannot disagree.

Try it before you commit to it

The playground's theme picker holds four example palettes — Nova, Ransom, Squamish, Void — and a Customise… dialog over every token in the theme. Edit a value and it applies live across every component while you browse; the CSS override tab is the same sheet as above, ready to paste into yours, and the Paste button reads one back in. The four palettes exist to prove a component reads from tokens rather than from our defaults — they are examples, not products.

Switching light and dark

Nothing in your app should write data-theme, the .dark class, or color-scheme by hand. ThemeProvider writes all three, and ThemeScript / initTheme do it before the first paint — see step 5 of the install guide.

When a token you want is missing

Add it to the registry rather than improvising: an opacity shortcut (bg-primary/10) composites against whatever happens to be behind it and cannot be retuned centrally, and a border borrowed from another family drifts the moment that family moves. Both are refused by tokens:check. Tailwind's own palette stays enabled as the escape hatch, and reads in a diff as exactly what it is — outside the design system, with no per-theme value, no hover rung and no asserted contrast.

Built by malinskibeniamin. The source code is available on GitHub.

On this page