Proper UI

Theming

Learn how to customize Proper UI components effortlessly to make them your own.

Every visual decision in Proper UI — color, spacing, radius, shadow, type scale — is a CSS variable defined once in theme.css and consumed by utility classes. Components never hard-code a color or a size, so re-theming the whole library is a matter of editing variables, not hunting through component files.

Token layers

The token system has three layers, each built on the one before it:

  1. Base palette — the raw --color-brand-50--color-brand-950 scale, plus Tailwind's built-in neutral/red/yellow/green/blue/… scales. This is the only layer you touch for a re-brand.
  2. Utility colors--color-utility-{brand,gray,blue,indigo,purple,pink,orange,…}-{50…900}, used by badges, tags, and charts that need a wider palette than the semantic layer exposes.
  3. Semantic tokens — role-based variables like --color-bg-primary, --color-text-secondary, --color-fg-brand-primary, and --color-border-tertiary, consumed directly by components as bg-primary, text-secondary, text-fg-brand-primary, border-tertiary. Semantic tokens reference the base and utility layers via var(), so changing the brand palette cascades automatically — including into dark mode.
PrefixRoleExample utility
text-*Typography colortext-primary, text-tertiary
fg-*Icon / graphic colortext-fg-quaternary
bg-*Surface colorbg-primary, bg-brand-solid
border-*Stroke colorborder-secondary, ring-primary

Never reach for a raw palette utility (bg-neutral-100, text-purple-600) inside a component — pick a semantic token by the role you need, not by the color it happens to render as today.

Editing the tokens

The CLI doesn't generate a brand scale for you — init writes theme.css with the default palette (or fetches it from the registry), and re-theming is a matter of editing that file directly:

/* theme.css */
@theme {
    --color-brand-25: #fcfaff;
    --color-brand-50: #f9f5ff;
    --color-brand-100: #f4ebff;
    /* … 200 through 900 … */
    --color-brand-950: #1a0966;
}

Keep the contrast relationships that the semantic layer expects: 600 is the primary interactive color on a light background, 700 is its hover state, 50/100 are tints for subtle surfaces, and 300 supplies the default focus-ring tone. Do not edit the semantic mappings themselves (bg-*, text-*, fg-*, border-*) — they're already wired to cascade correctly.

Scoping a theme to part of your app

Because every semantic token resolves through var(--color-brand-*), you can re-theme a single subtree by overriding the brand variables on a wrapper element instead of forking components:

.theme-productb {
    --color-brand-50: #eff6ff;
    --color-brand-600: #2563eb;
    --color-brand-700: #1d4ed8;
    /* …through 950 */
}
<div className="theme-productb">
    <Button color="primary">Uses the Product B brand</Button>
</div>

Everything inside .theme-productb — buttons, badges, focus rings, links — resolves to the overridden scale, while the rest of the page keeps the default brand. This is the same mechanism dark mode uses (see Dark mode), just scoped to a class you choose instead of .dark-mode.

FAQs