How the stylesheet layers compose
A reference for how @import order, :root, .dark, Tailwind layers, and component classes interact — and a debug checklist for when a token value isn't resolving the way you expect.
The cascade, in order
CSS resolves a token by walking the cascade from least-specific to most-specific. In app/globals.css, the 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 tokens yet.
- 2
tw-animate-css
@import 'tw-animate-css' — adds the data-state animation utilities that Radix-based components use for enter/exit transitions.
- 3
@theme inline
The @theme inline block maps raw token names (--background, --primary, …) into Tailwind's design system so utilities like bg-background and text-primary compile.
- 4
:root values
The :root block declares every light-mode token value: surfaces, foregrounds, brand colors, borders, status, charts, sizing, motion, z-index, and so on.
- 5
.dark values
The .dark { } block re-binds the mode-dependent tokens for dark mode. next-themes flips the .dark class on <html> before paint.
- 6
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 the right value lands wherever the component is rendered.
The most common bug when editing the stylesheet: putting a token value before the @theme inline mapping or outside the :root block. CSS walks top-to-bottom in source order; declarations that come first lose to declarations that come later.
The four debug scenarios
- Is the value inside the :root block in app/globals.css and after the @theme inline declaration? Earlier blocks lose to later ones.
- Are you binding --primary or --color-primary? In Tailwind v4 the @theme inline mapping is var(--primary) → --color-primary; both work, but stay consistent within a file.
- Are you targeting :root and not html? :root has higher specificity than html. Either works, but don't mix them in the same stylesheet.
- Did you place the value inside a media query or @layer block? Layered tokens lose to layer-less ones; declarations inside @media are mode-scoped.
- Is the value valid OKLCH? Run getComputedStyle(document.documentElement).getPropertyValue('--primary') in the console — an empty result means the value didn't parse.
- Is the value inside .dark { } and after the :root block? .dark must follow :root so its specificity wins when the class is present.
- Is next-themes actually toggling the .dark class? Inspect <html> in devtools — the class should flip between '' and 'dark'.
- Is suppressHydrationWarning set on <html>? Without it, next-themes triggers an SSR hydration mismatch that re-renders without the class.
- Are you reading resolvedTheme (not theme) when you need to know the current mode? theme can be 'system' even when resolvedTheme is 'dark'.
- Did you put dark values inside a @media (prefers-color-scheme: dark) block instead of .dark? next-themes uses the class, not the media query, so those values will never apply.
- Did the semantic pair get split? bg-card without text-card-foreground means the foreground falls back to whatever the parent sets.
- Is the className using a state-layer token where a surface was expected? bg-state-hover is a 4–6% layer — 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 a :root variable declaration. The class appears to 'win' because it reads var(--card) at use-time, so it always reflects the latest value.
- If a component applies bg-zinc-900 directly, that's a Tailwind palette literal and ignores the token entirely. Swap it for bg-card.
- If a component uses an arbitrary value like bg-[oklch(...)], it has hardcoded a value that won't respond to token changes. Use var(--card) or the token-arbitrary syntax bg-(--color-card).
- An !important on a utility (bg-card!) breaks the cascade for that property. Remove it; reach for state layers or the right role 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.228 0.013 107.4)" — declared ✓
// → "" — not declared. Check the source order in app/globals.css.
// 2. Is the variable resolving to the value you expect?
// Inspect element → Computed → search '--primary'. If it shows a stale
// value, an earlier declaration is shadowing the one you intended.
// 3. Is .dark actually applied?
document.documentElement.classList.contains('dark');
// → true / false. If false in dark mode, next-themes isn't toggling.
// 4. Does the component read the right value?
// Right-click the element → Inspect → Computed tab → search 'background'.
// It should read "background-color: oklch(...)" with the expected value.
// If it shows the inherited or default, the selector targets the wrong layer.
// 5. Is Tailwind compiling the class?
// Build for production, search dist/.next/static/css for 'bg-card'. If
// absent, the class isn't being scanned — check content paths.