PluginBench
Skill
Review
Audit score 70

tailwind-theme-builder

jezweb/claude-skills

Set up Tailwind v4 + shadcn/ui with CSS variable theming and dark mode in four mandatory steps.

What is tailwind-theme-builder?

Configures a fully themed Tailwind v4 project with shadcn/ui components and dark mode switching via CSS variables and @theme inline mapping. Use this when setting up Tailwind v4 theming, troubleshooting colour issues, migrating from v3, or implementing dark mode with semantic colour tokens.

  • Install Tailwind v4 with @tailwindcss/vite plugin and shadcn/ui dependencies
  • Configure CSS variables at :root and .dark with hsl() wrappers for semantic colours
  • Map variables to Tailwind utilities via @theme inline for automatic class generation
  • Set up ThemeProvider and dark mode toggle for seamless light/dark switching
  • Diagnose and fix 18 common gotchas including variable scope, @theme nesting, and double-wrapping errors

How to install tailwind-theme-builder

npx skills add https://github.com/jezweb/claude-skills --skill tailwind-theme-builder
Prerequisites
  • Node.js and pnpm (or npm/yarn)
  • Existing React + Vite project or willingness to create one
  • Familiarity with Tailwind CSS and CSS custom properties
Claude Code
Cursor
Windsurf
Cline

How to use tailwind-theme-builder

  1. 1.Run pnpm add tailwindcss @tailwindcss/vite and pnpm dlx shadcn@latest init to install dependencies
  2. 2.Add @tailwindcss/vite plugin to vite.config.ts and configure path alias for @/
  3. 3.Create src/index.css with :root and .dark CSS variables wrapped in hsl(), then add @theme inline mapping block
  4. 4.Copy theme-provider.tsx to components/ and wrap your app root, then add ModeToggle component with dropdown menu
  5. 5.Set components.json with config: "" (empty string) and verify dark mode toggle switches colours without re-render

Use cases

Good for
  • Starting a new Tailwind v4 + shadcn/ui project with dark mode support
  • Migrating an existing Tailwind v3 project to v4 theming architecture
  • Fixing broken colours after upgrading to Tailwind v4 or @apply errors
  • Implementing WCAG-compliant contrast ratios across light and dark themes
  • Troubleshooting @theme inline conflicts, tw-animate-css errors, or variable inheritance issues
Who it's for
  • Frontend developers building React apps with Tailwind v4
  • Teams adopting shadcn/ui component libraries with custom theming
  • Developers migrating from Tailwind v3 to v4
  • Anyone implementing accessible dark mode with CSS variables

tailwind-theme-builder FAQ

Why must :root be outside @layer base?

Tailwind v4 strips CSS outside @theme/@layer blocks. Placing :root inside @layer base causes variables to be ignored. Keep :root and .dark at root level, then reference them in @theme inline.

Do I need tailwind.config.ts in v4?

No. Delete it if it exists. Tailwind v4 uses @theme inline in CSS instead. If components.json points to a config file, set "config": "" (empty string).

Why aren't dark mode colours switching?

Most common cause: using .dark { @theme { } } instead of CSS variables. Use single @theme inline at root level, then override variables in .dark selector. Dark mode works by toggling .dark class on <html>.

What's the difference between @tailwindcss/vite and PostCSS?

v4 requires @tailwindcss/vite plugin for Vite. PostCSS plugin is deprecated. Add tailwindcss() to your Vite plugins array, not PostCSS config.

How do I add custom colours for charts or components?

Define CSS variables in :root/.dark with semantic names (e.g., --chart-1, --chart-2), map them in @theme inline, then reference via var(--chart-1) in style props or use bg-[var(--chart-1)] for dynamic utilities.

Full instructions (SKILL.md)

Source of truth, from jezweb/claude-skills.


name: tailwind-theme-builder description: "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." compatibility: claude-code-only

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

#SymptomCauseFix
1Variables ignored / theme broken:root inside @layer baseMove :root and .dark to root level
2Dark mode colours not switching.dark { @theme { } }Use CSS variables + single @theme inline
3Colours all black/whiteDouble hsl() wrappingUse var(--background) not hsl(var(...))
4bg-primary not generatedColours in tailwind.config.tsDelete config, use @theme inline
5bg-background class missingNo @theme inline blockAdd @theme inline mapping variables
6shadcn components breakcomponents.json has config pathSet "config": "" (empty string)
7Tailwind not processingUsing PostCSS pluginSwitch to @tailwindcss/vite plugin
8@/ imports failMissing path aliasesAdd paths to tsconfig.app.json
9Redundant dark: variantsUsing dark:bg-primary-darkJust use bg-primary -- variables handle it
10Hardcoded colours everywhereUsing bg-blue-600 dark:bg-blue-400Use semantic tokens: bg-primary
11Class merging bugsString concatenation for classesUse cn() from @/lib/utils
12Radix Select crashesEmpty string value value=""Use value="placeholder"
13Wrong Tailwind versionInstalled tailwindcss@^3Install tailwindcss@^4.1.0 + @tailwindcss/vite
14Missing peer depsOnly installed tailwindcssAlso install clsx, tailwind-merge, @types/node
15Broken in dark modeOnly tested light modeTest light, dark, system, and toggle transitions
16Fails WCAG contrastLooks fine visuallyCheck ratios: 4.5:1 normal text, 3:1 large/UI
17Build fails on animation importUsing tailwindcss-animate (deprecated)Use tw-animate-css or native CSS animations
18CSS priority issuesDuplicate @layer base after shadcn initMerge 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

More from jezweb/claude-skills and the wider catalog.

tailwind-v4-shadcn logo

tailwind-v4-shadcn

jezweb/claude-skills

Set up Tailwind v4 with shadcn/ui using CSS variables and @theme inline pattern, preventing 8 documented errors.

2.7k installsAudited
tanstack-query logo

tanstack-query

jezweb/claude-skills

Manage server state in React with TanStack Query v5—data fetching, caching, and synchronization.

2.6k installsAudited
tanstack-start logo

tanstack-start

jezweb/claude-skills

Generate full-stack TanStack Start apps on Cloudflare Workers with SSR, file-based routing, and built-in auth.

1.9k installs
team-update logo

team-update

jezweb/claude-skills

Post project updates to team chat, gather feedback, triage responses, and plan next steps. Adapts to available tools (chat, git, issues, tasks). First run discovers tools and saves a playbook; subsequent runs execute from the playbook. Trigger with 'team update', 'post update', 'sync with team', 'standup', 'check team chat', 'feedback loop', 'project update', 'what did the team say'.

782 installs
uk-business-english logo

uk-business-english

jezweb/claude-skills

British business English writing style for professional communications — polished, understated, EN-GB spelling (colour, organise, centre). Use whenever the user is writing for a British audience: emails, chat messages, proposals, client communications, blog posts, web copy, or any business writing. Apply to drafting, editing, and tone-checking professional text.

789 installsAudited
us-business-english logo

us-business-english

jezweb/claude-skills

American business English writing style for professional communications — direct, action-oriented, EN-US spelling (color, organize, center). Use whenever the user is writing for an American audience: emails, chat messages, proposals, client communications, blog posts, web copy, or any business writing. Apply to drafting, editing, and tone-checking professional text.

758 installsAudited