---
title: Theming your app
description: Restyle the whole system from one override sheet, and keep it honest as the registry moves.
icon: swatch-book
related:
  - /theme
  - /interaction-states
---

Every colour, radius and shadow a component paints comes from a token in [`theme.css`](/docs/theme), 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.

```bash
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:

```css title="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:

```css title="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](/docs/theme#building-something-new) 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

```json title="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.

| flag | default |
| --- | --- |
| `--overrides` | required — the sheet to check |
| `--theme` | `../theme.css` beside the script, which is where `@redpanda/theme` installs it |
| `--palette` | comma-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.

:::info
Runs under `bun`, or `node` 24+, which strips the TypeScript itself.
:::

## Try it before you commit to it

The [playground](https://redpanda-ui-playground.netlify.app)'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](/docs/install#5-apply-the-theme-before-the-first-paint).

## 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.
