Agent Skills

tailwind-theme-builder

frontendjezweb3.1K installs

Set up Tailwind v4 + shadcn/ui themed UI with dark mode. Install deps, configure CSS variables via @theme inline, wire dark mode toggle, verify. Use whenever the user mentions Tailwind v4, setting up Tailwind theming, shadcn/ui colours, dark mode, or troubleshooting colours not working, tw-animate-css errors, @theme inline conflicts, @apply breaking after upgrade, or v3 → v4 migration issues.

Install

npx skills add https://github.com/jezweb/claude-skills --skill tailwind-theme-builder
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 && 'active'}`} CORRECT: className={cn("base", isActive && "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

Related skills

vercel-react-best-practicesvercel-labs751KReact and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.vercel-composition-patternsvercel-labs361KReact composition patterns that scale. Use when refactoring components with boolean prop proliferation, building flexible component libraries, or designing reusable APIs. Triggers on tasks involving compound components, render props, context providers, or component architecture. Includes React 19 API changes.vercel-react-view-transitionsvercel-labs135KGuide for implementing smooth, native-feeling animations using React's View Transition API (`<ViewTransition>` component, `addTransitionType`, and CSS view transition pseudo-elements). Use this skill whenever the user wants to add page transitions, animate route changes, create shared element animations, animate enter/exit of components, animate list reorder, implement directional (forward/back) navigation animations, or integrate view transitions in Next.js. Also use when the user mentions viewpick-ui-libraryemilkowalski124KPick the right library for a given frontend task from a curated, opinionated list — numbers, OTP inputs, charts, command menus, virtualization, drag and drop, toasts, state, styling, and more. Only runs when explicitly invoked; it does not trigger on its own.

Search skills and MCP servers

Fuzzy search across 23,137 skills and servers