tailwind-theme-builder

Tailwind v4 and shadcn/ui setup with CSS variables, dark mode, and semantic color theming. Automates dependency installation, Vite configuration with the Tailwind plugin, and shadcn/ui initialization Implements a mandatory four-step CSS architecture: root CSS variables, @theme inline mapping, base styles, and dark mode via class switching Includes pre-built theme provider component for dark mode toggling and persistent theme storage Provides troubleshooting guides for common v4 migration issues: @apply breaking changes, double hsl() wrapping, missing @theme blocks, and tw-animate-css errors

INSTALLATION
npx skills add https://github.com/jezweb/claude-skills --skill tailwind-theme-builder
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

Tailwind Theme Builder

Set up a fully themed Tailwind v4 + shadcn/ui project with dark mode. Produces configured CSS, theme provider, and working component library.

Architecture: The Four-Step Pattern

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

How It Works

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 :root)

  --> Tailwind utilities reference updated variables

  --> UI updates without re-render

Best Practices

  • Semantic names: Use --primary not --blue-500
  • Foreground pairing: Every background colour needs a foreground (--primary + --primary-foreground)
  • WCAG contrast: Normal text 4.5:1, large text 3:1, UI components 3:1
  • Chart colours: Use separate variables with @theme inline mapping, reference via var(--chart-1) in style props

Workflow

Step 1: Install Dependencies

pnpm add tailwindcss @tailwindcss/vite

pnpm add -D @types/node tw-animate-css

pnpm dlx shadcn@latest init

# Delete v3 config if it exists

rm -f tailwind.config.ts

Step 2: Configure Vite

Copy assets/vite.config.ts or add the Tailwind plugin:

import { defineConfig } from 'vite'

import react from '@vitejs/plugin-react'

import tailwindcss from '@tailwindcss/vite'

import path from 'path'

export default defineConfig({

  plugins: [react(), tailwindcss()],

  resolve: { alias: { '@': path.resolve(__dirname, './src') } }

})

Step 3: Four-Step CSS Architecture (Mandatory)

This exact order is required. Skipping steps breaks the theme.

src/index.css:

@import "tailwindcss";

@import "tw-animate-css";

/* 1. Define CSS variables at root (NOT inside @layer base) */

:root {

  --background: hsl(0 0% 100%);

  --foreground: hsl(222.2 84% 4.9%);

  --primary: hsl(221.2 83.2% 53.3%);

  --primary-foreground: hsl(210 40% 98%);

  /* ... all semantic tokens */

}

.dark {

  --background: hsl(222.2 84% 4.9%);

  --foreground: hsl(210 40% 98%);

  --primary: hsl(217.2 91.2% 59.8%);

  --primary-foreground: hsl(222.2 47.4% 11.2%);

}

/* 2. Map variables to Tailwind utilities */

@theme inline {

  --color-background: var(--background);

  --color-foreground: var(--foreground);

  --color-primary: var(--primary);

  --color-primary-foreground: var(--primary-foreground);

}

/* 3. Apply base styles (NO hsl() wrapper here) */

@layer base {

  body {

    background-color: var(--background);

    color: var(--foreground);

  }

}

Result: bg-background, text-primary etc. work automatically. Dark mode switches via .dark class -- no dark: variants needed for semantic colours.

Step 4: Set Up Dark Mode

Copy assets/theme-provider.tsx to your components directory, then wrap your app:

import { ThemeProvider } from '@/components/theme-provider'

ReactDOM.createRoot(document.getElementById('root')!).render(

  <ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">

    <App />

  </ThemeProvider>

)

Add a theme toggle -- install the dropdown menu then use the ModeToggle component below:

pnpm dlx shadcn@latest add dropdown-menu
// src/components/mode-toggle.tsx

import { Moon, Sun } from "lucide-react"

import { Button } from "@/components/ui/button"

import {

  DropdownMenu,

  DropdownMenuContent,

  DropdownMenuItem,

  DropdownMenuTrigger,

} from "@/components/ui/dropdown-menu"

import { useTheme } from "@/components/theme-provider"

export function ModeToggle() {

  const { setTheme } = useTheme()

  return (

    <DropdownMenu>

      <DropdownMenuTrigger asChild>

        <Button variant="outline" size="icon">

          <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />

          <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />

          <span className="sr-only">Toggle theme</span>

        </Button>

      </DropdownMenuTrigger>

      <DropdownMenuContent align="end">

        <DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>

        <DropdownMenuItem onClick={() => setTheme("dark")}>Dark</DropdownMenuItem>

        <DropdownMenuItem onClick={() => setTheme("system")}>System</DropdownMenuItem>

      </DropdownMenuContent>

    </DropdownMenu>

  )

}

Step 5: Configure components.json

{

  "tailwind": {

    "config": "",

    "css": "src/index.css",

    "baseColor": "slate",

    "cssVariables": true

  }

}

"config": "" is critical -- v4 doesn't use tailwind.config.ts.

Critical Rules

Always:

  • Wrap colours with hsl() in :root/.dark
  • Use @theme inline to map all CSS variables
  • Use @tailwindcss/vite plugin (NOT PostCSS)
  • Delete tailwind.config.ts if it exists

Never:

  • Put :root/.dark inside @layer base
  • Use .dark { @theme { } } (v4 doesn't support nested @theme)
  • Double-wrap: hsl(var(--background))
  • Use @apply with @layer base classes (use @utility instead)

All 18 Gotchas

Quick Diagnosis

#

Symptom

Cause

Fix

1

Variables ignored / theme broken

:root inside @layer base

Move :root and .dark to root level

2

Dark mode colours not switching

.dark { @theme { } }

Use CSS variables + single @theme inline

3

Colours all black/white

Double hsl() wrapping

Use var(--background) not hsl(var(...))

4

bg-primary not generated

Colours in tailwind.config.ts

Delete config, use @theme inline

5

bg-background class missing

No @theme inline block

Add @theme inline mapping variables

6

shadcn components break

components.json has config path

Set "config": "" (empty string)

7

Tailwind not processing

Using PostCSS plugin

Switch to @tailwindcss/vite plugin

8

@/ imports fail

Missing path aliases

Add paths to tsconfig.app.json

9

Redundant dark: variants

Using dark:bg-primary-dark

Just use bg-primary -- variables handle it

10

Hardcoded colours everywhere

Using bg-blue-600 dark:bg-blue-400

Use semantic tokens: bg-primary

11

Class merging bugs

String concatenation for classes

Use cn() from @/lib/utils

12

Radix Select crashes

Empty string value value=""

Use value="placeholder"

13

Wrong Tailwind version

Installed tailwindcss@^3

Install tailwindcss@^4.1.0 + @tailwindcss/vite

14

Missing peer deps

Only installed tailwindcss

Also install clsx, tailwind-merge, @types/node

15

Broken in dark mode

Only tested light mode

Test light, dark, system, and toggle transitions

16

Fails WCAG contrast

Looks fine visually

Check ratios: 4.5:1 normal text, 3:1 large/UI

17

Build fails on animation import

Using tailwindcss-animate (deprecated)

Use tw-animate-css or native CSS animations

18

CSS priority issues

Duplicate @layer base after shadcn init

Merge into single @layer base block

Gotcha Details with Code Examples

#1 -- :root inside @layer base

Tailwind v4 strips CSS outside @theme/@layer, but :root must be at root level to persist. This is the most common setup failure.

WRONG:

@layer base {

  :root { --background: hsl(0 0% 100%); }

}

CORRECT:

:root { --background: hsl(0 0% 100%); }

@layer base {

  body { background-color: var(--background); }

}

#2 -- Nested @theme

Tailwind v4 does not support @theme inside selectors. Use CSS variables in :root/.dark with a single @theme inline block.

WRONG:

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

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

CORRECT:

:root { --primary: hsl(0 0% 0%); }

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

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

#3 -- Double hsl() wrapping

Variables already contain hsl(). Double-wrapping creates hsl(hsl(...)).

WRONG: background-color: hsl(var(--background));

CORRECT: background-color: var(--background);

#4 -- Colours in tailwind.config.ts

Tailwind v4 completely ignores theme.extend.colors in config files. Delete the file or leave it empty. Set "config": "" in components.json.

#5 -- Missing @theme inline

Without @theme inline, Tailwind has no knowledge of your CSS variables. Utility classes like bg-background simply won't be generated.

WRONG:

:root { --background: hsl(0 0% 100%); }

/* No @theme inline block -- bg-background won't exist */

CORRECT:

:root { --background: hsl(0 0% 100%); }

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

#7 -- PostCSS vs Vite plugin

WRONG:

export default defineConfig({

  css: { postcss: './postcss.config.js' }  // Old v3 way

})

CORRECT:

import tailwindcss from '@tailwindcss/vite'

export default defineConfig({

  plugins: [react(), tailwindcss()]  // v4 way

})

#8 -- Path aliases

Add to tsconfig.app.json:

{

  "compilerOptions": {

    "baseUrl": ".",

    "paths": { "@/*": ["./src/*"] }

  }

}

#11 -- cn() utility for class merging

WRONG: className={base ${isActive &#x26;&#x26; 'active'}}

CORRECT: className={cn("base", isActive &#x26;&#x26; "active")}

cn() from @/lib/utils properly merges and deduplicates Tailwind classes.

#12 -- Radix Select empty value

Radix UI Select does not allow empty string values. Use value="placeholder" instead of value="".

#14 -- Required dependencies

{

  "dependencies": {

    "tailwindcss": "^4.1.0",

    "@tailwindcss/vite": "^4.1.0",

    "clsx": "^2.1.1",

    "tailwind-merge": "^3.3.1"

  },

  "devDependencies": {

    "@types/node": "^24.0.0"

  }

}

#17 -- tw-animate-css

tailwindcss-animate is deprecated in Tailwind v4. shadcn/ui docs may still reference it. Causes build failures and import errors. Use tw-animate-css or @tailwindcss/motion instead.

#18 -- Duplicate @layer base after shadcn init

shadcn init adds its own @layer base block. Check src/index.css immediately after running init and merge any duplicate blocks into one.

WRONG:

@layer base { body { background-color: var(--background); } }

@layer base { * { border-color: hsl(var(--border)); } }  /* duplicate from shadcn */

CORRECT:

@layer base {

  * { border-color: var(--border); }

  body { background-color: var(--background); color: var(--foreground); }

}

Prevention Checklist

  • No tailwind.config.ts file (or it's empty)
  • components.json has "config": ""
  • All colors have hsl() wrapper in :root
  • @theme inline maps all variables
  • @layer base doesn't wrap :root
  • Theme provider wraps app
  • Tested in light, dark, and system modes
  • All text has sufficient contrast

Dark Mode Testing Checklist

  • Light mode displays correctly
  • Dark mode displays correctly
  • System mode respects OS setting
  • Theme persists after page refresh
  • Toggle component shows current state
  • All text has proper contrast
  • No flash of wrong theme on load
  • Works in incognito mode (graceful fallback)

Asset Files

Copy from assets/ directory:

  • index.css -- Complete CSS with all colour variables
  • components.json -- shadcn/ui v4 config
  • vite.config.ts -- Vite + Tailwind plugin
  • theme-provider.tsx -- Dark mode provider
  • utils.ts -- cn() utility

Reference Files

  • references/migration-guide.md -- v3 to v4 migration

Official Documentation

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card