Files
2025-11-30 08:25:32 +08:00

4.2 KiB

Tailwind v4 + shadcn/ui Theming Architecture

The Four-Step Pattern

Tailwind v4 requires a specific architecture for CSS variable-based theming. This pattern is mandatory - skipping or modifying steps will break your theme.

Step 1: Define CSS Variables at Root Level

:root {
  --background: hsl(0 0% 100%);
  --foreground: hsl(222.2 84% 4.9%);
  /* ... more colors */
}

.dark {
  --background: hsl(222.2 84% 4.9%);
  --foreground: hsl(210 40% 98%);
  /* ... dark mode colors */
}

Critical Rules:

  • Define at root level (NOT inside @layer base)
  • Use hsl() wrapper on all color values
  • Use .dark for dark mode overrides (NOT .dark { @theme { } })
  • Never put :root or .dark inside @layer base

Step 2: Map Variables to Tailwind Utilities

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  /* ... map all CSS variables */
}

Why This Is Required:

  • Tailwind v4 doesn't read tailwind.config.ts for colors
  • @theme inline generates utility classes (bg-background, text-foreground)
  • Without this, utilities like bg-primary won't exist

Step 3: Apply Base Styles

@layer base {
  body {
    background-color: var(--background);  /* NO hsl() wrapper here */
    color: var(--foreground);
  }
}

Critical Rules:

  • Reference variables directly: var(--background)
  • Never double-wrap: hsl(var(--background)) (already has hsl)

Step 4: Result - Automatic Dark Mode

With this architecture:

  • <div className="bg-background text-foreground"> works automatically
  • No dark: variants needed in components
  • Theme switches via .dark class on <html>
  • Single source of truth for all colors

Why This Architecture Works

Color Variable Flow

CSS Variable Definition → @theme inline Mapping → Tailwind Utility Class
--background           → --color-background     → bg-background
(with hsl() wrapper)     (references variable)    (generated class)

Dark Mode Switching

ThemeProvider toggles `.dark` class on <html>
  ↓
CSS variables update automatically (.dark overrides)
  ↓
Tailwind utilities reference updated variables
  ↓
UI updates without re-render

Common Mistakes

Mistake 1: Variables Inside @layer base

/* WRONG */
@layer base {
  :root {
    --background: hsl(0 0% 100%);
  }
}

Why It Fails: Tailwind v4 strips CSS outside @theme/@layer, but :root must be at root level to persist.

Mistake 2: Using .dark { @theme { } }

/* WRONG */
@theme {
  --color-primary: hsl(0 0% 0%);
}

.dark {
  @theme {
    --color-primary: hsl(0 0% 100%);
  }
}

Why It Fails: Tailwind v4 doesn't support nested @theme directives.

Mistake 3: Double hsl() Wrapping

/* WRONG */
@layer base {
  body {
    background-color: hsl(var(--background));
  }
}

Why It Fails: --background already contains hsl(), results in hsl(hsl(...)).

Mistake 4: Config-Based Colors

// WRONG (tailwind.config.ts)
export default {
  theme: {
    extend: {
      colors: {
        primary: 'hsl(var(--primary))'
      }
    }
  }
}

Why It Fails: Tailwind v4 completely ignores theme.extend.colors in config files.


Best Practices

1. Semantic Color Names

Use semantic names, not color values:

--primary      /* ✅ Semantic */
--blue-500     /* ❌ Not semantic */

2. Foreground Pairing

Every background color needs a foreground:

--primary: hsl(...);
--primary-foreground: hsl(...);

3. WCAG Contrast Ratios

Ensure proper contrast:

  • Normal text: 4.5:1 minimum
  • Large text: 3:1 minimum
  • UI components: 3:1 minimum

4. Chart Colors

Charts need separate variables (don't use hsl wrapper in components):

:root {
  --chart-1: hsl(12 76% 61%);
}

@theme inline {
  --color-chart-1: var(--chart-1);
}

Use in components:

<div style={{ backgroundColor: 'var(--chart-1)' }} />

Official Documentation