Reskin in ≤10 lines
Consumer projects override at most ten CSS variables per theme block. Token names and structure stay identical to the canonical theme — that is what keeps the system coherent across products.
Anatomy of an override
A reskin is a single globals.css file that declares two blocks: :root for light mode and .dark for dark mode. Both reference the same token names — never invent new ones, never rename.
@import "@kaotypr-ui/theme";
:root {
--primary: oklch(0.205 0.04 290); /* deep indigo */
--secondary: oklch(0.86 0.07 50); /* peach */
--background: oklch(0.99 0.005 80); /* warm off-white */
--foreground: oklch(0.18 0 0);
--border: oklch(0.92 0.01 80);
--radius-card: 1.25rem;
--radius-button: 0.75rem;
--font-heading: var(--font-inter);
--color-chart-1: oklch(0.65 0.13 50); /* anchor charts to peach */
}
.dark {
--background: oklch(0.13 0.01 290);
--foreground: oklch(0.96 0 0);
--primary: oklch(0.92 0 0);
--secondary: oklch(0.43 0.07 50);
--border: oklch(0.27 0.01 290);
}What you can override
- Surfaces:
--background,--card,--popover - Foreground hierarchy:
--foreground,--foreground-subtle - Brand:
--primary,--secondary,--accent - Status lightness/chroma (hue stays locked)
- Fonts:
--font-display,--font-heading,--font-sans - Semantic radii:
--radius-button,--radius-card, … - Chart palette:
--color-chart-1..12
- Shadows:
--shadow-card,--shadow-popover - Motion:
--duration-*,--ease-* - Overlay opacity:
--overlay - Per-control surfaces (switch, slider, scrollbar, skeleton)
- Link / code / kbd treatments
- Blur scale:
--blur-sm,--blur-md,--blur-lg
These keep the system coherent across every Kaotypr product. Overriding any of them locally is a bug, not a customization.
- Z-index ladder (cross-product layering)
- State-layer opacities (4 / 6 / 8 / 12 / 16%)
- Status hue conventions (red = destructive, green = success, yellow = warning, blue = info)
--ring-width= 3px- The
--size-*ratio scale (control / icon / avatar / touch-target) - The opacity scale (
--opacity-*)
If you find yourself needing to override one of these, file an issue in the design-system repo. It is almost certainly a system-wide need.
Validation checklist
After applying your override, walk through these. Each one catches a different class of mistake.
Status colors still read as themselves
Destructive must read as red. Success as green. Warning as yellow/orange. Info as blue. If your reskin pushed --primary into the red space, you must not also let it cascade into status colors.
Focus rings are still 3px
Tab through your interactive surfaces. The ring is uniform across buttons, inputs, links, sidebar items. Width is locked; only color flips with theme.
Compact density still tightens
Toggle data-density='compact' on <html>. Controls and card paddings shrink predictably. Touch targets stay ≥ 44px.
Chart series 1 anchors to brand
The first series in any chart should read as your accent. Override --color-chart-1 to your primary anchor; the rest of the palette derives from there.
Light and dark both legible
Switch between modes. Foreground/background contrast stays ≥ 4.5:1 for body copy in both modes. Status pairs (e.g. bg-success + text-success-foreground) remain readable.
No locked tokens in your override
grep your override for --z-index-, --ring-width, --opacity-, --state-, --size-. If any appear, remove them or open an issue.
Anti-patterns
The reskins that go wrong tend to go wrong the same way.
Overriding --primary AND status colors so 'destructive' is no longer red
Status hue is a cross-product convention. Users in product A and product B both expect red = destructive. Pick a primary that doesn't collide with red — or accept that status colors stay where they are.
Setting --radius-card and --radius-dialog to wildly different scales
The semantic-radii relationship is part of the system identity. If --radius-card is 1rem, --radius-dialog should land near 1.25rem, not 2.5rem.
Defining new token names like --brand-red or --custom-spacing
The token surface is fixed. New tokens at the consumer layer fragment the system. Compose with existing tokens or use a CSS variable scoped to your component (--my-component-thing).
Heavy shadow-card with low contrast borders
Shadows are a tunable tier — but they only feel right when they pair with the surface contrast they were drawn against. If you crank shadow + drop border-subtle, cards float disconnected.
Overriding inside a component with !important
Reskins live at the :root / .dark level. Overriding inside a component bypasses the cascade and breaks every consumer that builds on top.
Different values for the same token in :root vs .dark that change meaning
If --primary is 'brand indigo' in light, it can't be 'success green' in dark. Light and dark are tonal variants of the same role; the role doesn't change.
Common reskin directions
Four off-the-shelf starting points. Each is a complete :root + .dark block — copy the one closest to your brand and tune from there.
:root {
--background: oklch(0.99 0.005 80);
--foreground: oklch(0.18 0 0);
--primary: oklch(0.55 0.16 30); /* terracotta */
--secondary: oklch(0.86 0.07 50); /* peach */
--border: oklch(0.92 0.01 80);
--color-chart-1: oklch(0.55 0.16 30);
}
.dark {
--background: oklch(0.16 0.01 30);
--foreground: oklch(0.96 0 0);
--primary: oklch(0.78 0.13 30);
--secondary: oklch(0.50 0.08 50);
}Splash variations
Some products warrant tonal variants beyond brand color: a tinted background, a colored card surface, custom popover treatments. Override --background, --card or --popover directly — these are brand values, not locked. Just keep the foreground pair intact (a tinted card needs a paired --card-foreground).