
Tailwind V4 Shadcn
- 6.9k installs
- 196 repo stars
- Updated July 25, 2026
- secondsky/claude-skills
tailwind-v4-shadcn is an agent skill for | Production-tested setup for Tailwind CSS v4 with shadcn/ui, Vite, and React. Use when: initializing React projects with Tailwind v4, setting up shadcn/ui, implementing dark mode
About
Production-tested setup for Tailwind CSS v4 with shadcn ui Vite and React Use when initializing React projects with Tailwind v4 setting up shadcn ui implementing dark mode debugging CSS variable issues fixing theme switching migrating from Tailwind v3 or encountering color theming problems Covers theme inline pattern CSS variable architecture dark mode with ThemeProvider componen name tailwind-v4-shadcn description Production-tested setup for Tailwind CSS v4 with shadcn ui Vite and React Use when initializing React projects with Tailwind v4 setting up shadcn ui implementing dark mode debugging CSS variable issues fixing theme switching migrating from Tailwind v3 or encountering color theming problems Covers theme inline pattern CSS variable architecture dark mode with ThemeProvider component composition vite config setup common v4 gotchas and production-tested patterns metadata keywords Tailwind v4 shadcn ui tailwindcss vite theme inline dark mode CSS variables hsl wrapper components json React theming theme switching colors not working variables broken theme not applying plugin directive typography plugin forms plugin prose class tailwindcss typography tailwindcss forms license M.
- "@tailwindcss/typography"
- Tailwind v4 + shadcn/ui Production Stack
- [Before You Start](#-before-you-start-read-this)
- [Quick Start](#quick-start-5-minutes---follow-this-exact-order)
- [Four-Step Architecture](#the-four-step-architecture-critical)
Tailwind V4 Shadcn by the numbers
- 6,896 all-time installs (skills.sh)
- +433 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #103 of 1,879 Marketing & SEO skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 29, 2026 (Skillselion catalog sync)
tailwind-v4-shadcn capabilities & compatibility
- Capabilities
- "@tailwindcss/typography" · tailwind v4 + shadcn/ui production stack · [before you start](# before you start read this) · [quick start](#quick start 5 minutes follow th · [four step architecture](#the four step architec
- Use cases
- documentation
What tailwind-v4-shadcn says it does
--- name: tailwind-v4-shadcn description: "| Production-tested setup for Tailwind CSS v4 with shadcn/ui, Vite, and React.
[Before You Start](#-before-you-start-read-this) 2.
[Quick Start](#quick-start-5-minutes---follow-this-exact-order) 3.
npx skills add https://github.com/secondsky/claude-skills --skill tailwind-v4-shadcnAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 6.9k |
|---|---|
| repo stars | ★ 196 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 25, 2026 |
| Repository | secondsky/claude-skills ↗ |
When should developers use tailwind-v4-shadcn and what problem does it solve?
| Production-tested setup for Tailwind CSS v4 with shadcn/ui, Vite, and React. Use when: initializing React projects with Tailwind v4, setting up shadcn/ui, implementing dark mode, debugging CSS varia
Who is it for?
Developers working with tailwind-v4-shadcn patterns described in the skill documentation.
Skip if: Skip when cached docs are empty or the task is outside the skill's documented scope.
When should I use this skill?
| Production-tested setup for Tailwind CSS v4 with shadcn/ui, Vite, and React. Use when: initializing React projects with Tailwind v4, setting up shadcn/ui, implementing dark mode, debugging CSS varia
What you get
Grounded guidance and workflows from SKILL.md for tailwind-v4-shadcn.
- Theme CSS with semantic variables
- Component class composition examples
Files
Tailwind v4 + shadcn/ui Production Stack
Production-tested: WordPress Auditor (https://wordpress-auditor.webfonts.workers.dev) Last Updated: 2025-12-04 Status: Production Ready ✅
Table of Contents
1. Before You Start 2. Quick Start 3. Four-Step Architecture 4. Dark Mode Setup 5. Critical Rules 6. Semantic Color Tokens 7. Common Issues & Fixes 8. File Templates 9. Setup Checklist 10. Advanced Topics 11. Dependencies 12. Tailwind v4 Plugins 13. Reference Documentation 14. When to Load References
---
⚠️ BEFORE YOU START (READ THIS!)
CRITICAL FOR AI AGENTS: If you're Claude Code helping a user set up Tailwind v4:
1. Explicitly state you're using this skill at the start of the conversation 2. Reference patterns from the skill rather than general knowledge 3. Prevent known issues listed in reference/common-gotchas.md 4. Don't guess - if unsure, check the skill documentation
USER ACTION REQUIRED: Tell Claude to check this skill first!
Say: "I'm setting up Tailwind v4 + shadcn/ui - check the tailwind-v4-shadcn skill first"
Why This Matters (Real-World Results)
Without skill activation:
- ❌ Setup time: ~5 minutes
- ❌ Errors encountered: 2-3 (tw-animate-css, duplicate @layer base)
- ❌ Manual fixes needed: 2+ commits
- ❌ Token usage: ~65k
- ❌ User confidence: Required debugging
With skill activation:
- ✅ Setup time: ~1 minute
- ✅ Errors encountered: 0
- ✅ Manual fixes needed: 0
- ✅ Token usage: ~20k (70% reduction)
- ✅ User confidence: Instant success
Known Issues This Skill Prevents
1. tw-animate-css import error (deprecated in v4) 2. Duplicate @layer base blocks (shadcn init adds its own) 3. Wrong template selection (vanilla TS vs React) 4. Missing post-init cleanup (incompatible CSS rules) 5. Wrong plugin syntax (using @import or require() instead of @plugin directive)
All of these are handled automatically when the skill is active.
---
Quick Start (5 Minutes - Follow This Exact Order)
1. Install Dependencies
bun add tailwindcss @tailwindcss/vite
# or: npm install tailwindcss @tailwindcss/vite
bun add -d @types/node
# Note: Using pnpm for shadcn init due to known Bun compatibility issues
# (bunx has "Script not found" and postinstall/msw problems)
pnpm dlx shadcn@latest init2. Configure Vite
// vite.config.ts
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')
}
}
})3. Update components.json
{
"tailwind": {
"config": "", // ← CRITICAL: Empty for v4
"css": "src/index.css",
"cssVariables": true
}
}4. Delete tailwind.config.ts
rm tailwind.config.ts # v4 doesn't use this file---
The Four-Step Architecture (CRITICAL)
This pattern is mandatory - skipping steps will break your theme.
Step 1: Define CSS Variables at Root Level
/* src/index.css */
@import "tailwindcss";
:root {
--background: hsl(0 0% 100%); /* ← hsl() wrapper required */
--foreground: hsl(222.2 84% 4.9%);
--primary: hsl(221.2 83.2% 53.3%);
/* ... all light mode colors */
}
.dark {
--background: hsl(222.2 84% 4.9%);
--foreground: hsl(210 40% 98%);
--primary: hsl(217.2 91.2% 59.8%);
/* ... all dark mode colors */
}Critical Rules:
- ✅ Define at root level (NOT inside
@layer base) - ✅ Use
hsl()wrapper on all color values - ✅ Use
.darkfor dark mode (NOT.dark { @theme { } })
Step 2: Map Variables to Tailwind Utilities
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
/* ... map ALL CSS variables */
}Why This Is Required:
- Generates utility classes (
bg-background,text-primary) - Without this,
bg-primaryetc. won't exist
Step 3: Apply Base Styles
@layer base {
body {
background-color: var(--background); /* NO hsl() here */
color: var(--foreground);
}
}Critical Rules:
- ✅ Reference variables directly:
var(--background) - ❌ Never double-wrap:
hsl(var(--background))
Step 4: Result - Automatic Dark Mode
<div className="bg-background text-foreground">
{/* No dark: variants needed - theme switches automatically */}
</div>---
Dark Mode Setup
1. Create ThemeProvider
See reference/dark-mode.md for full implementation or use template:
// Copy from: templates/theme-provider.tsx2. Wrap Your App
// src/main.tsx
import { ThemeProvider } from '@/components/theme-provider'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<App />
</ThemeProvider>
</React.StrictMode>,
)3. Add Theme Toggle
pnpm dlx shadcn@latest add dropdown-menuSee reference/dark-mode.md for ModeToggle component code.
---
Critical Rules (MUST FOLLOW)
✅ Always Do:
1. Wrap color values with `hsl()` in `:root` and `.dark`
--background: hsl(0 0% 100%); /* ✅ Correct */2. Use `@theme inline` to map all CSS variables
@theme inline {
--color-background: var(--background);
}3. Set `"tailwind.config": ""` in components.json
{ "tailwind": { "config": "" } }4. Delete `tailwind.config.ts` if it exists
5. Use `@tailwindcss/vite` plugin (NOT PostCSS)
6. Use `cn()` for conditional classes
import { cn } from "@/lib/utils"
<div className={cn("base", isActive && "active")} />❌ Never Do:
1. Put `:root` or `.dark` inside `@layer base`
/* WRONG */
@layer base {
:root { --background: hsl(...); }
}2. Use `.dark { @theme { } }` pattern
/* WRONG - v4 doesn't support nested @theme */
.dark {
@theme {
--color-primary: hsl(...);
}
}3. Double-wrap colors
/* WRONG */
body {
background-color: hsl(var(--background));
}4. Use `tailwind.config.ts` for theme colors
/* WRONG - v4 ignores this */
export default {
theme: {
extend: {
colors: { primary: 'hsl(var(--primary))' }
}
}
}5. Use `@apply` directive (deprecated in v4)
6. Use `dark:` variants for semantic colors
/* WRONG */
<div className="bg-primary dark:bg-primary-dark" />
/* CORRECT */
<div className="bg-primary" />---
Semantic Color Tokens
Always use semantic names for colors:
:root {
--destructive: hsl(0 84.2% 60.2%); /* Red - errors, critical */
--success: hsl(142.1 76.2% 36.3%); /* Green - success states */
--warning: hsl(38 92% 50%); /* Yellow - warnings */
--info: hsl(221.2 83.2% 53.3%); /* Blue - info, primary */
}Usage:
<div className="bg-destructive text-destructive-foreground">Critical</div>
<div className="bg-success text-success-foreground">Success</div>
<div className="bg-warning text-warning-foreground">Warning</div>
<div className="bg-info text-info-foreground">Info</div>---
Common Issues & Quick Fixes
| Symptom | Cause | Fix |
|---|---|---|
bg-primary doesn't work | Missing @theme inline mapping | Add @theme inline block |
| Colors all black/white | Double hsl() wrapping | Use var(--color) not hsl(var(--color)) |
| Dark mode not switching | Missing ThemeProvider | Wrap app in <ThemeProvider> |
| Build fails | tailwind.config.ts exists | Delete the file |
| Text invisible | Wrong contrast colors | Check color definitions in :root/.dark |
See reference/common-gotchas.md for complete troubleshooting guide.
---
File Templates
All templates are available in the templates/ directory:
- index.css - Complete CSS setup with all color variables
- components.json - shadcn/ui v4 configuration
- vite.config.ts - Vite + Tailwind plugin setup
- tsconfig.app.json - TypeScript with path aliases
- theme-provider.tsx - Dark mode provider with localStorage
- utils.ts -
cn()utility for class merging
Copy these files to your project and customize as needed.
---
Complete Setup Checklist
- [ ] Vite + React + TypeScript project created
- [ ]
@tailwindcss/viteinstalled (NOT postcss) - [ ]
vite.config.tsusestailwindcss()plugin - [ ]
tsconfig.jsonhas path aliases configured - [ ]
components.jsonexists with"config": "" - [ ] NO
tailwind.config.tsfile exists - [ ]
src/index.cssfollows v4 pattern: - [ ]
:rootand.darkat root level (not in @layer) - [ ] Colors wrapped with
hsl() - [ ]
@theme inlinemaps all variables - [ ]
@layer baseuses unwrapped variables - [ ] Theme provider installed and wrapping app
- [ ] Dark mode toggle component created
- [ ] Test theme switching works in browser
---
Advanced Topics
Load references/advanced-usage.md for advanced patterns including:
- Custom Colors: Add semantic colors beyond default palette
- v3 Migration: See
references/migration-guide.mdfor complete guide - Component Best Practices: Semantic tokens, cn() utility, composition patterns
Quick Example:
:root { --brand: hsl(280 65% 60%); }
@theme inline { --color-brand: var(--brand); }Usage: <div className="bg-brand">Branded</div>
For detailed patterns and component composition examples, load references/advanced-usage.md.
---
Dependencies
✅ Install These
{
"dependencies": {
"tailwindcss": "^4.1.17",
"@tailwindcss/vite": "^4.1.17",
"clsx": "^2.1.1",
"tailwind-merge": "^3.3.1",
"@radix-ui/react-*": "latest",
"lucide-react": "^0.554.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@types/node": "^24.10.1",
"@vitejs/plugin-react": "^5.1.1",
"vite": "^7.2.4",
"typescript": "~5.9.3"
}
}❌ NEVER Install These (Deprecated in v4)
# These packages will cause build errors:
bun add tailwindcss-animate # ❌ Deprecated
# or: npm install tailwindcss-animate # ❌ Deprecated
bun add tw-animate-css # ❌ Doesn't existIf you see import errors for these packages, remove them and use native CSS animations or @tailwindcss/motion instead.
---
Tailwind v4 Plugins
Tailwind v4 supports official plugins using the @plugin directive in CSS.
Quick Example:
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";Common Error: ❌ WRONG: @import "@tailwindcss/typography" (doesn't work) ✅ CORRECT: @plugin "@tailwindcss/typography" (use @plugin directive)
Built-in Features: Container queries are now core (no @tailwindcss/container-queries plugin needed).
Load references/plugins-reference.md for complete documentation including Typography plugin (prose classes), Forms plugin, installation steps, and common plugin errors.
---
Reference Documentation
For deeper understanding, see:
- common-gotchas.md - All the ways it can break (and fixes)
- dark-mode.md - Complete dark mode implementation
- migration-guide.md - Migrating hardcoded colors to CSS variables
- plugins-reference.md - Official Tailwind v4 plugins (Typography, Forms)
- advanced-usage.md - Custom colors and advanced patterns
---
When to Load References
Load reference files based on user's specific needs:
Load references/common-gotchas.md when:
- User reports "colors not working" or "bg-primary doesn't exist"
- Dark mode not switching properly
- Build fails with Tailwind errors
- User encounters any CSS/configuration issue
- Debugging theme problems
Load references/dark-mode.md when:
- User asks to implement dark mode
- Theme switching not working
- Need ThemeProvider component code
- Questions about system theme detection
Load references/migration-guide.md when:
- Migrating from Tailwind v3 to v4
- User has hardcoded colors to migrate
- Questions about v3 → v4 changes
- Need migration checklist
Load references/plugins-reference.md when:
- User needs Typography plugin (prose class)
- User needs Forms plugin
- Questions about @plugin directive
- Plugin installation errors
Load references/advanced-usage.md when:
- User asks about custom colors beyond defaults
- Need advanced component patterns
- Questions about component best practices
- Component composition questions
---
Official Documentation
- shadcn/ui Vite Setup: https://ui.shadcn.com/docs/installation/vite
- shadcn/ui Tailwind v4 Guide: https://ui.shadcn.com/docs/tailwind-v4
- shadcn/ui Dark Mode (Vite): https://ui.shadcn.com/docs/dark-mode/vite
- Tailwind v4 Docs: https://tailwindcss.com/docs
- shadcn/ui Theming: https://ui.shadcn.com/docs/theming
---
Secure Installation
When installing Tailwind and shadcn/ui packages, follow supply chain security best practices:
- Block post-install scripts —
npm config set ignore-scripts true(or Bun: disabled by default) - Cooldown period — Wait 7 days for new package versions to be vetted by the community
- Audit before installing — Run
socket package score npm <pkg>or usesocket npm install <pkg>to check packages
Load the dependency-upgrade skill for full security configuration including Socket CLI integration, cooldown setup, lockfile validation, and CI enforcement.
Production Example
This skill is based on the WordPress Auditor project:
- Live: https://wordpress-auditor.webfonts.workers.dev
- Stack: Vite + React 19 + Tailwind v4 + shadcn/ui + Cloudflare Workers
- Dark Mode: Full system/light/dark support
- Version: Tailwind v4.1.17 + shadcn/ui latest (Nov 2025)
All patterns in this skill have been validated in production.
---
Questions? Issues?
1. Check reference/common-gotchas.md first 2. Verify all steps in the 4-step architecture 3. Ensure components.json has "config": "" 4. Delete tailwind.config.ts if it exists 5. Check official docs: https://ui.shadcn.com/docs/tailwind-v4
Advanced Usage Patterns
Purpose: Advanced customization and component patterns for experienced Tailwind v4 + shadcn/ui developers When to Load: User asks for custom colors beyond defaults, advanced component patterns, composition best practices, or component customization
---
Custom Colors
Add new semantic colors beyond the default palette:
:root {
--brand: hsl(280 65% 60%);
--brand-foreground: hsl(0 0% 100%);
}
.dark {
--brand: hsl(280 75% 70%);
--brand-foreground: hsl(280 20% 10%);
}
@theme inline {
--color-brand: var(--brand);
--color-brand-foreground: var(--brand-foreground);
}Usage:
<div className="bg-brand text-brand-foreground">Branded Component</div>Key Pattern: Define CSS variable in :root/.dark, then reference in @theme inline with --color- prefix.
---
Migration from Tailwind v3
For complete v3 → v4 migration steps, see references/migration-guide.md.
Quick Summary:
- Remove
tailwind.config.js(v4 uses CSS configuration) - Convert hardcoded colors to CSS variables
- Update plugin syntax:
require('tailwindcss/plugin')(plugin)in v3 config →@plugin "plugin-name"in CSS - Change Vite plugin:
require('tailwindcss')orimport tailwindcss from 'tailwindcss'in v3 →import tailwindcss from '@tailwindcss/vite'in v4
---
Component Best Practices
1. Always Use Semantic Tokens
✅ CORRECT:
<Button variant="destructive">Delete</Button>❌ WRONG:
<Button className="bg-red-600">Delete</Button>Why: Semantic tokens (destructive, primary, secondary) adapt to theme changes. Hardcoded colors break dark mode and theme customization.
---
2. Use cn() for Conditional Styling
Import:
import { cn } from "@/lib/utils"Usage:
<div className={cn(
"base-class",
isActive && "active-class",
hasError && "error-class"
)} />What `cn()` does: Merges Tailwind classes intelligently (later classes override earlier ones).
---
3. Compose shadcn/ui Components
Pattern:
<Dialog>
<DialogTrigger asChild>
<Button>Open</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Title</DialogTitle>
</DialogHeader>
</DialogContent>
</Dialog>Key Concept: Use composition (wrapping components) rather than customization (passing props).
---
Advanced Patterns
Conditional Theme Variables
Apply different variables based on state:
<div className={cn(
"rounded-lg p-4",
variant === "success" && "bg-success text-success-foreground",
variant === "error" && "bg-destructive text-destructive-foreground"
)} />Custom Radius Tokens
Add semantic border radius:
@theme inline {
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
--radius-lg: 1rem;
}Usage: className="rounded-[var(--radius-lg)]"
Component Variants Pattern
Use cva() from class-variance-authority for complex variants:
import { cva } from "class-variance-authority"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground",
destructive: "bg-destructive text-destructive-foreground",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 px-3",
lg: "h-11 px-8",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)---
Official Resources
- Tailwind v4 Docs: https://tailwindcss.com/blog/tailwindcss-v4-beta
- shadcn/ui: https://ui.shadcn.com
- class-variance-authority: https://cva.style/docs
Common Gotchas & Solutions
Critical Failures (Will Break Your Build)
1. :root Inside @layer base
❌ WRONG:
@layer base {
:root {
--background: hsl(0 0% 100%);
}
}✅ CORRECT:
:root {
--background: hsl(0 0% 100%);
}
@layer base {
body {
background-color: var(--background);
}
}Why: Tailwind v4 strips CSS outside @theme/@layer, but :root must be at root level.
---
2. Nested @theme Directive
❌ 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);
}Why: Tailwind v4 doesn't support @theme inside selectors.
---
3. Double hsl() Wrapping
❌ WRONG:
@layer base {
body {
background-color: hsl(var(--background));
}
}✅ CORRECT:
@layer base {
body {
background-color: var(--background); /* Already has hsl() */
}
}Why: Variables already contain hsl(), double-wrapping creates hsl(hsl(...)).
---
4. Colors in tailwind.config.ts
❌ WRONG:
// tailwind.config.ts
export default {
theme: {
extend: {
colors: {
primary: 'hsl(var(--primary))'
}
}
}
}✅ CORRECT:
// Delete tailwind.config.ts entirely OR leave it empty
export default {}
// components.json
{
"tailwind": {
"config": "" // ← Empty string
}
}Why: Tailwind v4 completely ignores theme.extend.colors.
---
5. Missing @theme inline Mapping
❌ WRONG:
:root {
--background: hsl(0 0% 100%);
}
/* No @theme inline block */Result: bg-background class doesn't exist
✅ CORRECT:
:root {
--background: hsl(0 0% 100%);
}
@theme inline {
--color-background: var(--background);
}Why: @theme inline generates the utility classes.
---
Configuration Gotchas
6. Wrong components.json Config
❌ WRONG:
{
"tailwind": {
"config": "tailwind.config.ts" // ← No!
}
}✅ CORRECT:
{
"tailwind": {
"config": "" // ← Empty for v4
}
}---
7. Using PostCSS Instead of Vite Plugin
❌ WRONG:
// vite.config.ts
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. Missing Path Aliases
❌ WRONG:
// tsconfig.json has no paths
import { Button } from '../../components/ui/button'✅ CORRECT:
// tsconfig.app.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}import { Button } from '@/components/ui/button'---
Color System Gotchas
9. Using dark: Variants for Semantic Colors
❌ WRONG:
<div className="bg-primary dark:bg-primary-dark" />✅ CORRECT:
<div className="bg-primary" />Why: With proper CSS variable setup, bg-primary automatically responds to theme.
---
10. Hardcoded Color Values
❌ WRONG:
<div className="bg-blue-600 dark:bg-blue-400" />✅ CORRECT:
<div className="bg-primary" /> {/* Or bg-info, bg-success, etc. */}Why: Semantic tokens enable theme switching and reduce repetition.
---
Component Gotchas
11. Missing cn() Utility
❌ WRONG:
<div className={`base ${isActive && 'active'}`} />✅ CORRECT:
import { cn } from '@/lib/utils'
<div className={cn("base", isActive && "active")} />Why: cn() properly merges and deduplicates Tailwind classes.
---
12. Empty String in Radix Select
❌ WRONG:
<SelectItem value="">Select an option</SelectItem>✅ CORRECT:
<SelectItem value="placeholder">Select an option</SelectItem>Why: Radix UI Select doesn't allow empty string values.
---
Installation Gotchas
13. Wrong Tailwind Package
❌ WRONG:
npm install tailwindcss@^3.4.0 # v3✅ CORRECT:
npm install tailwindcss@^4.1.0 # v4
npm install @tailwindcss/vite---
14. Missing Dependencies
❌ WRONG:
{
"dependencies": {
"tailwindcss": "^4.1.0"
// Missing @tailwindcss/vite
}
}✅ CORRECT:
{
"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 Import Error (REAL-WORLD ISSUE)
❌ WRONG:
npm install tailwindcss-animate # Deprecated package@import "tw-animate-css"; # Package doesn't exist in v4✅ CORRECT:
# Don't install tailwindcss-animate at all
# Use native CSS animations or @tailwindcss/motionWhy:
tailwindcss-animateis deprecated in Tailwind v4- Causes import errors during build
- shadcn/ui docs may still reference it (outdated)
- The skill handles animations differently in v4
Impact: Build failure, requires manual CSS file cleanup
---
18. Duplicate @layer base After shadcn init (REAL-WORLD ISSUE)
❌ WRONG:
/* After running shadcn init, you might have: */
@layer base {
body {
background-color: var(--background);
}
}
@layer base { /* ← Duplicate added by shadcn init */
* {
border-color: hsl(var(--border));
}
}✅ CORRECT:
/* Merge into single @layer base block */
@layer base {
* {
border-color: var(--border);
}
body {
background-color: var(--background);
color: var(--foreground);
}
}Why:
shadcn initadds its own@layer baseblock- Results in duplicate layer declarations
- Can cause unexpected CSS priority issues
- Easy to miss during setup
Prevention:
- Check
src/index.cssimmediately after runningshadcn init - Merge any duplicate
@layer baseblocks - Keep only one base layer section
Impact: CSS priority issues, harder to debug styling problems
---
Testing Gotchas
15. Not Testing Both Themes
❌ WRONG: Only testing in light mode
✅ CORRECT: Test in:
- Light mode
- Dark mode
- System mode
- Both initial load and toggle
---
16. Not Checking Contrast
❌ WRONG: Colors look good but fail WCAG
✅ CORRECT:
- Use browser DevTools Lighthouse
- Check contrast ratios (4.5:1 minimum)
- Test with actual users
---
Quick Diagnosis
Symptoms → Likely Cause:
| Symptom | Likely Cause |
|---|---|
bg-primary doesn't work | Missing @theme inline mapping |
| Colors all black/white | Double hsl() wrapping |
| Dark mode not switching | Missing ThemeProvider |
| Build fails | tailwind.config.ts exists with theme config |
| Text invisible | Wrong contrast colors |
@/ imports fail | Missing path aliases in tsconfig |
---
Prevention Checklist
Before deploying:
- [ ] No
tailwind.config.tsfile (or it's empty) - [ ]
components.jsonhas"config": "" - [ ] All colors have
hsl()wrapper in:root - [ ]
@theme inlinemaps all variables - [ ]
@layer basedoesn't wrap:root - [ ] Theme provider wraps app
- [ ] Tested in both light and dark modes
- [ ] All text has sufficient contrast
Dark Mode Implementation
Overview
Tailwind v4 + shadcn/ui dark mode requires: 1. ThemeProvider component to manage state 2. .dark class toggling on <html> element 3. localStorage persistence 4. System theme detection
---
ThemeProvider Component
Full Implementation
// src/components/theme-provider.tsx
import { createContext, useContext, useEffect, useState, ReactNode } from 'react'
type Theme = 'dark' | 'light' | 'system'
type ThemeProviderProps = {
children: ReactNode
defaultTheme?: Theme
storageKey?: string
}
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
const initialState: ThemeProviderState = {
theme: 'system',
setTheme: () => null,
}
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({
children,
defaultTheme = 'system',
storageKey = 'vite-ui-theme',
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(() => {
try {
return (localStorage.getItem(storageKey) as Theme) || defaultTheme
} catch (e) {
return defaultTheme
}
})
useEffect(() => {
const root = window.document.documentElement
root.classList.remove('light', 'dark')
if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
.matches ? 'dark' : 'light'
root.classList.add(systemTheme)
return
}
root.classList.add(theme)
}, [theme])
const value = {
theme,
setTheme: (theme: Theme) => {
try {
localStorage.setItem(storageKey, theme)
} catch (e) {
console.warn('Storage unavailable')
}
setTheme(theme)
},
}
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
if (context === undefined)
throw new Error('useTheme must be used within a ThemeProvider')
return context
}Wrap Your App
// src/main.tsx
import { ThemeProvider } from '@/components/theme-provider'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<App />
</ThemeProvider>
</React.StrictMode>,
)---
Theme Toggle Component
Using shadcn/ui Dropdown Menu
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>
)
}---
How It Works
Theme Flow
User selects theme → setTheme() called
↓
Save to localStorage
↓
Update state
↓
useEffect triggers
↓
Remove existing classes (.light, .dark)
↓
Add new class to <html>
↓
CSS variables update (.dark overrides :root)
↓
UI updates automaticallySystem Theme Detection
if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
.matches ? 'dark' : 'light'
root.classList.add(systemTheme)
}This respects the user's OS preference when "System" is selected.
---
Common Issues
Issue: Dark mode not switching
Cause: Theme provider not wrapping app Fix: Ensure <ThemeProvider> wraps your app in main.tsx
Issue: Theme resets on page refresh
Cause: localStorage not working Fix: Check browser privacy settings, add sessionStorage fallback
Issue: Flash of wrong theme on load
Cause: Theme applied after initial render Fix: Add inline script to index.html (advanced)
Issue: Icons not changing
Cause: CSS transitions not working Fix: Verify icon classes use dark: variants for animations
---
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)
---
Official Documentation
- shadcn/ui Dark Mode (Vite): https://ui.shadcn.com/docs/dark-mode/vite
- Tailwind Dark Mode: https://tailwindcss.com/docs/dark-mode
Migration Guide: Hardcoded Colors → CSS Variables
Overview
This guide helps you migrate from hardcoded Tailwind colors (bg-blue-600) to semantic CSS variables (bg-primary).
Benefits:
- Automatic dark mode support
- Consistent color usage
- Single source of truth
- Easy theme customization
- Better accessibility
---
Semantic Color Mapping
| Hardcoded Color | CSS Variable | Use Case |
|---|---|---|
bg-red-* / text-red-* | bg-destructive / text-destructive | Critical issues, errors, delete actions |
bg-green-* / text-green-* | bg-success / text-success | Success states, positive metrics |
bg-yellow-* / text-yellow-* | bg-warning / text-warning | Warnings, moderate issues |
bg-blue-* / text-blue-* | bg-info or bg-primary | Info boxes, primary actions |
bg-gray-* / text-gray-* | bg-muted / text-muted-foreground | Backgrounds, secondary text |
bg-purple-* | bg-info | Remove - use blue instead |
bg-orange-* | bg-warning | Remove - use yellow instead |
bg-emerald-* | bg-success | Remove - use green instead |
---
Migration Patterns
Pattern 1: Solid Backgrounds
❌ Before:
<div className="bg-blue-50 dark:bg-blue-950/20 text-blue-700 dark:text-blue-300">✅ After:
<div className="bg-info/10 text-info">Note: /10 creates 10% opacity
---
Pattern 2: Borders
❌ Before:
<div className="border-2 border-green-200 dark:border-green-800">✅ After:
<div className="border-2 border-success/30">---
Pattern 3: Text Colors
❌ Before:
<span className="text-red-600 dark:text-red-400">✅ After:
<span className="text-destructive">---
Pattern 4: Icons
❌ Before:
<AlertCircle className="text-yellow-500" />✅ After:
<AlertCircle className="text-warning" />---
Pattern 5: Gradients
❌ Before:
<div className="bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-950/20 dark:to-emerald-950/20">✅ After:
<div className="bg-gradient-to-r from-success/10 to-success/20">---
Step-by-Step Migration
Step 1: Add Semantic Colors to CSS
/* src/index.css */
:root {
/* Add these if not already present */
--destructive: hsl(0 84.2% 60.2%);
--destructive-foreground: hsl(210 40% 98%);
--success: hsl(142.1 76.2% 36.3%);
--success-foreground: hsl(210 40% 98%);
--warning: hsl(38 92% 50%);
--warning-foreground: hsl(222.2 47.4% 11.2%);
--info: hsl(221.2 83.2% 53.3%);
--info-foreground: hsl(210 40% 98%);
}
.dark {
--destructive: hsl(0 62.8% 30.6%);
--destructive-foreground: hsl(210 40% 98%);
--success: hsl(142.1 70.6% 45.3%);
--success-foreground: hsl(222.2 47.4% 11.2%);
--warning: hsl(38 92% 55%);
--warning-foreground: hsl(222.2 47.4% 11.2%);
--info: hsl(217.2 91.2% 59.8%);
--info-foreground: hsl(222.2 47.4% 11.2%);
}
@theme inline {
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
}Step 2: Find Hardcoded Colors
# Search for background colors
grep -r "bg-\(red\|yellow\|blue\|green\|purple\|orange\|pink\|emerald\)-[0-9]" src/
# Search for text colors
grep -r "text-\(red\|yellow\|blue\|green\|purple\|orange\|pink\|emerald\)-[0-9]" src/
# Search for border colors
grep -r "border-\(red\|yellow\|blue\|green\|purple\|orange\|pink\|emerald\)-[0-9]" src/Step 3: Replace Component by Component
Start with high-impact components: 1. Buttons 2. Badges 3. Alert boxes 4. Status indicators 5. Cards
Step 4: Test Both Themes
After each component:
- [ ] Check light mode appearance
- [ ] Check dark mode appearance
- [ ] Verify text contrast
- [ ] Test hover/active states
---
Example: Badge Component
❌ Before:
const severityConfig = {
critical: {
color: 'text-red-500',
bg: 'bg-red-500/10',
border: 'border-red-500/20',
},
warning: {
color: 'text-yellow-500',
bg: 'bg-yellow-500/10',
border: 'border-yellow-500/20',
},
info: {
color: 'text-blue-500',
bg: 'bg-blue-500/10',
border: 'border-blue-500/20',
}
}✅ After:
const severityConfig = {
critical: {
color: 'text-destructive',
bg: 'bg-destructive/10',
border: 'border-destructive/20',
},
warning: {
color: 'text-warning',
bg: 'bg-warning/10',
border: 'border-warning/20',
},
info: {
color: 'text-info',
bg: 'bg-info/10',
border: 'border-info/20',
}
}---
Testing Checklist
After migration:
- [ ] All severity levels (critical/warning/info) visually distinct
- [ ] Text has proper contrast in both light and dark modes
- [ ] No hardcoded color classes remain
- [ ] Hover states work correctly
- [ ] Gradients render smoothly
- [ ] Icons are visible and colored correctly
- [ ] Borders are visible
- [ ] No visual regressions
---
Verification Commands
# Should return 0 results when migration complete
grep -r "text-red-[0-9]" src/components/
grep -r "bg-blue-[0-9]" src/components/
grep -r "border-green-[0-9]" src/components/
# Verify semantic colors are used
grep -r "bg-destructive" src/components/
grep -r "text-success" src/components/---
Performance Impact
Before: Every component has dark: variants
<div className="bg-blue-50 dark:bg-blue-950/20 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-800">After: Single class, CSS handles switching
<div className="bg-info/10 text-info border-info/30">Result:
- 60% fewer CSS classes in markup
- Smaller HTML payload
- Faster rendering
- Easier to maintain
---
Common Pitfalls
1. Forgetting to Map in @theme inline
Variables defined in :root but not mapped → utilities don't exist
2. Wrong Opacity Syntax
❌ bg-success-10 (doesn't work) ✅ bg-success/10 (correct)
3. Mixing Approaches
Don't mix hardcoded and semantic in same component - choose one approach.
4. Not Testing Dark Mode
Always test both themes during migration.
---
Rollback Plan
If migration causes issues:
1. Keep original components in git history 2. Use feature flags to toggle new theme 3. Test with subset of users first 4. Have monitoring for visual regressions
---
Further Customization
After migration, you can easily:
- Add new semantic colors
- Create theme variants (high contrast, etc.)
- Support multiple brand themes
- Implement user-selectable color schemes
All by editing CSS variables - no component changes needed!
Tailwind v4 Plugins Reference
Purpose: Complete guide to Tailwind v4 official plugins (Typography, Forms) When to Load: User mentions prose class, Typography plugin, Forms plugin, @plugin directive, or plugin installation errors
---
Overview
Tailwind v4 supports official plugins using the @plugin directive in CSS (not the v3 config file approach).
---
Official Plugins (Tailwind Labs)
Typography Plugin - Style Markdown/CMS Content
When to use: Displaying blog posts, documentation, or any HTML from Markdown/CMS.
Installation:
bun add -d @tailwindcss/typography
# or: npm install -D @tailwindcss/typographyConfiguration (v4 syntax):
/* src/index.css */
@import "tailwindcss";
@plugin "@tailwindcss/typography";Usage:
<article class="prose lg:prose-xl dark:prose-invert">
{{ markdown_content }}
</article>Available classes:
prose- Base typography stylesprose-sm,prose-base,prose-lg,prose-xl,prose-2xl- Size variantsdark:prose-invert- Dark mode styles
---
Forms Plugin - Reset Form Element Styles
When to use: Building custom forms without shadcn/ui components, or need consistent cross-browser form styling.
Installation:
bun add -d @tailwindcss/forms
# or: npm install -D @tailwindcss/formsConfiguration (v4 syntax):
/* src/index.css */
@import "tailwindcss";
@plugin "@tailwindcss/forms";What it does:
- Resets browser default form styles
- Makes form elements styleable with Tailwind utilities
- Fixes cross-browser inconsistencies for inputs, selects, checkboxes, radios
Note: Less critical for shadcn/ui users (they have pre-styled form components), but still useful for basic forms.
---
Common Plugin Errors
These errors happen when using v3 syntax in v4 projects:
Error 1: Using v3 config file syntax
❌ WRONG (v3 config file syntax):
// tailwind.config.js
module.exports = {
plugins: [require('@tailwindcss/typography')]
}Why it fails: Tailwind v4 doesn't use tailwind.config.js for plugins.
✅ CORRECT (v4 @plugin directive):
/* src/index.css */
@import "tailwindcss";
@plugin "@tailwindcss/typography";---
Error 2: Using @import instead of @plugin
❌ WRONG (@import instead of @plugin):
@import "@tailwindcss/typography"; /* Doesn't work */Why it fails: Plugins must be loaded with @plugin, not @import.
✅ CORRECT:
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";---
Multiple Plugins
Load multiple plugins by adding multiple @plugin directives:
/* src/index.css */
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";Order matters: Import tailwindcss first, then plugins.
---
Official Documentation
- Typography: https://tailwindcss.com/docs/typography-plugin
- Forms: https://tailwindcss.com/docs/plugins#official-plugins
- Tailwind v4 Plugin System: https://tailwindcss.com/blog/tailwindcss-v4-beta#css-first-configuration
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
@import "tailwindcss";
/*
Tailwind v4 + shadcn/ui Dark Mode Pattern
Based on: https://ui.shadcn.com/docs/tailwind-v4
Key Pattern:
1. Define CSS variables at root level (NOT in @layer base)
2. Use .dark for dark mode overrides (NOT in @theme)
3. Use @theme inline to map variables to Tailwind utilities
4. All color values must use hsl() wrapper
*/
/* Light mode colors - Define at root level */
:root {
--background: hsl(0 0% 100%);
--foreground: hsl(222.2 84% 4.9%);
--card: hsl(0 0% 100%);
--card-foreground: hsl(222.2 84% 4.9%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(222.2 84% 4.9%);
--primary: hsl(221.2 83.2% 53.3%);
--primary-foreground: hsl(210 40% 98%);
--secondary: hsl(210 40% 96.1%);
--secondary-foreground: hsl(222.2 47.4% 11.2%);
--muted: hsl(210 40% 96.1%);
--muted-foreground: hsl(215.4 16.3% 46.9%);
--accent: hsl(210 40% 96.1%);
--accent-foreground: hsl(222.2 47.4% 11.2%);
--destructive: hsl(0 84.2% 60.2%);
--destructive-foreground: hsl(210 40% 98%);
--success: hsl(142.1 76.2% 36.3%);
--success-foreground: hsl(210 40% 98%);
--warning: hsl(38 92% 50%);
--warning-foreground: hsl(222.2 47.4% 11.2%);
--info: hsl(221.2 83.2% 53.3%);
--info-foreground: hsl(210 40% 98%);
--border: hsl(214.3 31.8% 91.4%);
--input: hsl(214.3 31.8% 91.4%);
--ring: hsl(221.2 83.2% 53.3%);
--radius: 0.5rem;
/* Chart colors */
--chart-1: hsl(12 76% 61%);
--chart-2: hsl(173 58% 39%);
--chart-3: hsl(197 37% 24%);
--chart-4: hsl(43 74% 66%);
--chart-5: hsl(27 87% 67%);
}
/* Dark mode colors - Plain CSS overrides (NOT in @theme) */
.dark {
--background: hsl(222.2 84% 4.9%);
--foreground: hsl(210 40% 98%);
--card: hsl(222.2 84% 4.9%);
--card-foreground: hsl(210 40% 98%);
--popover: hsl(222.2 84% 4.9%);
--popover-foreground: hsl(210 40% 98%);
--primary: hsl(217.2 91.2% 59.8%);
--primary-foreground: hsl(222.2 47.4% 11.2%);
--secondary: hsl(217.2 32.6% 17.5%);
--secondary-foreground: hsl(210 40% 98%);
--muted: hsl(217.2 32.6% 17.5%);
--muted-foreground: hsl(215 20.2% 65.1%);
--accent: hsl(217.2 32.6% 17.5%);
--accent-foreground: hsl(210 40% 98%);
--destructive: hsl(0 62.8% 30.6%);
--destructive-foreground: hsl(210 40% 98%);
--success: hsl(142.1 70.6% 45.3%);
--success-foreground: hsl(222.2 47.4% 11.2%);
--warning: hsl(38 92% 55%);
--warning-foreground: hsl(222.2 47.4% 11.2%);
--info: hsl(217.2 91.2% 59.8%);
--info-foreground: hsl(222.2 47.4% 11.2%);
--border: hsl(217.2 32.6% 17.5%);
--input: hsl(217.2 32.6% 17.5%);
--ring: hsl(224.3 76.3% 48%);
/* Chart colors for dark mode */
--chart-1: hsl(220 70% 50%);
--chart-2: hsl(160 60% 45%);
--chart-3: hsl(30 80% 55%);
--chart-4: hsl(280 65% 60%);
--chart-5: hsl(340 75% 55%);
}
/* Map CSS variables to Tailwind theme with @theme inline */
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
/* Border radius tokens */
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
}
/* Base styles in @layer base */
@layer base {
* {
border-color: var(--border);
}
body {
margin: 0;
background-color: var(--background);
color: var(--foreground);
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
import { createContext, useContext, useEffect, useState, ReactNode } from 'react'
type Theme = 'dark' | 'light' | 'system'
type ThemeProviderProps = {
children: ReactNode
defaultTheme?: Theme
storageKey?: string
}
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
const initialState: ThemeProviderState = {
theme: 'system',
setTheme: () => null,
}
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({
children,
defaultTheme = 'system',
storageKey = 'vite-ui-theme',
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(() => {
// Try localStorage first, fall back to sessionStorage, then default
try {
return (localStorage.getItem(storageKey) as Theme) ||
(sessionStorage.getItem(storageKey) as Theme) ||
defaultTheme
} catch (e) {
// Storage unavailable (incognito/privacy mode) - use default
return defaultTheme
}
})
useEffect(() => {
const root = window.document.documentElement
root.classList.remove('light', 'dark')
if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
.matches
? 'dark'
: 'light'
root.classList.add(systemTheme)
return
}
root.classList.add(theme)
}, [theme])
const value = {
theme,
setTheme: (theme: Theme) => {
// Try to persist to localStorage, fall back to sessionStorage
try {
localStorage.setItem(storageKey, theme)
} catch (e) {
// localStorage unavailable (incognito) - use sessionStorage
try {
sessionStorage.setItem(storageKey, theme)
} catch (err) {
// Both unavailable - just update state without persistence
console.warn('Storage unavailable, theme preference will not persist')
}
}
setTheme(theme)
},
}
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
if (context === undefined)
throw new Error('useTheme must be used within a ThemeProvider')
return context
}
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Path aliases */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import path from 'path'
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
})
Related skills
FAQ
What does tailwind-v4-shadcn do?
| Production-tested setup for Tailwind CSS v4 with shadcn/ui, Vite, and React. Use when: initializing React projects with Tailwind v4, setting up shadcn/ui, implementing dark mode, debugging CSS variable issues, fixing t
When should I invoke tailwind-v4-shadcn?
| Production-tested setup for Tailwind CSS v4 with shadcn/ui, Vite, and React. Use when: initializing React projects with Tailwind v4, setting up shadcn/ui, implementing dark mode, debugging CSS variable issues, fixing t
Where is the source documentation?
Ground claims in SKILL.md excerpts and linked reference files from the cached docs.
Is Tailwind V4 Shadcn safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.