Why your override isn't working
Most reskins go wrong in one of four places. This page is the debug checklist when --primary refuses to change, .dark doesn't apply, or Tailwind seems to ignore your token. Read once, save the bookmark.
The cascade, in order
CSS resolves a token by walking the cascade from least-specific to most-specific. In a Kaotypr consumer app, layers stack like this:
- 1
Tailwind base reset
@import 'tailwindcss' — applies preflight, normalize, and the default --spacing scale. This is the floor; nothing above it cares about your tokens yet.
- 2
Kaotypr theme
@import '@kaotypr-ui/theme' — declares every token under :root (light values) and .dark (dark values). This is the canonical theme.
- 3
Your :root overrides
The block in your project's globals.css that re-binds brand-tier tokens (--primary, --background, --radius-card, etc.). Must come AFTER the @import — order in the file matters.
- 4
Your .dark overrides
The .dark { } block that re-binds the dark-mode values for the same tokens. next-themes flips the .dark class on <html> before paint.
- 5
Component classNames
When a component reads bg-card or rounded-button, Tailwind v4 resolves the var(--card) / var(--radius-button) reference at use-time — so your override is already baked in.
The single most common bug: putting your overrides before the @import. The cascade walks top-to-bottom in source order; an override that fires first is simply replaced by the import that comes after it.
The four debug scenarios
- Is your :root block AFTER @import '@kaotypr-ui/theme' in source order? Move it below.
- Are you setting --primary or --color-primary? In Tailwind v4 the @theme inline mapping is var(--primary) → --color-primary; both work, but be consistent — pick one and stop.
- Are you targeting :root and not html? :root has higher specificity than html. Either works, but never mix.
- Did you put the override inside a media query (@media (min-width: ...)) or layer? Layered tokens lose to layer-less ones.
- Is the value valid OKLCH? Run getComputedStyle(document.documentElement).getPropertyValue('--primary') in the console — if it returns empty, the value parsed wrong.
- Is the override under .dark { } and AFTER your :root block? .dark must follow :root in source order so its specificity wins when the class is present.
- Is next-themes actually toggling the .dark class? Inspect <html> in devtools — class should flip between '' and 'dark'.
- Is suppressHydrationWarning on <html>? Without it next-themes triggers an SSR hydration mismatch that re-renders without the class.
- Are you using resolvedTheme (not theme) when reading the mode? theme can be 'system' even when resolvedTheme is 'dark'.
- Did you put :root values inside a @media (prefers-color-scheme: dark) block instead of .dark? next-themes uses the class, not the media query — those overrides will never fire.
- Did you split the semantic pair? bg-card without text-card-foreground means the foreground falls back to whatever the parent sets.
- Are you using a state-layer token where you wanted a surface? bg-state-hover is a 4–6% layer, NOT a surface — on its own it looks transparent.
- Is a parent class overriding via specificity? An ancestor with text-foreground/60 will leak down to children that don't set their own foreground.
- Is Tailwind v4 finding the class? Token-arbitrary syntax like text-(--color-foreground) requires the variable to be declared in @theme or :root — confirm with devtools.
- Is dark mode flipping the wrong way? Check that --card-foreground in .dark contrasts with --card in .dark, not against the light-mode --card.
- Tailwind utilities are class selectors (.bg-card) — same specificity as your :root variable (an attribute-like declaration). The class WINS because it's later in the cascade and uses var(--card) at use-time. So the utility didn't 'win'; it read the latest var().
- If you're applying bg-zinc-900 directly to a component, that's a Tailwind palette literal and ignores your token entirely. Swap it for bg-card.
- If you're using arbitrary values bg-[oklch(...)] you've hardcoded a value that won't respond to your override. Use var(--card) or token-arbitrary syntax bg-(--color-card).
- Consumer-side !important on a utility (.bg-card!) breaks the cascade for that property. Remove it; reach for state layers or proper roles instead.
The 60-second debug recipe
When something's off and you don't know where to start, run this sequence in order. The first failing step is your bug.
// 1. Is the variable declared at all?
getComputedStyle(document.documentElement).getPropertyValue('--primary');
// → "oklch(0.205 0.04 290)" — declared ✓
// → "" — not declared. Check @import order.
// 2. Is the variable resolving to your override or the canonical theme value?
// Inspect element → Computed → search '--primary'. If it's the canonical
// value, your :root block came BEFORE the @import. Move it down.
// 3. Is .dark actually applied?
document.documentElement.classList.contains('dark');
// → true / false. If false in dark mode, next-themes is broken.
// 4. Does the component read your override?
// Right-click the element → Inspect → Computed tab → search 'background'.
// You should see "background-color: oklch(...)" with your value.
// If it shows the inherited / default, your selector targets the wrong layer.
// 5. Is Tailwind purging the class?
// Build prod, search dist/.next/static/css for 'bg-card'. If absent,
// the class isn't being scanned — check content paths in your tailwind config.