
Tailwind V4 Shadcn
- 115 installs
- 51 repo stars
- Updated November 25, 2025
- ovachiever/droid-tings
Extends Claude Code with specialized agent capabilities for developer workflows.
About
Skill for Tailwind CSS v4 and shadcn/ui component development workflows in Claude Code.
- Agent skill
- Developer productivity
- Workflow automation
Tailwind V4 Shadcn by the numbers
- 115 all-time installs (skills.sh)
- Ranked #3,881 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ovachiever/droid-tings --skill tailwind-v4-shadcnAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 115 |
|---|---|
| repo stars | ★ 51 |
| Last updated | November 25, 2025 |
| Repository | ovachiever/droid-tings ↗ |
What it does
Extends Claude Code with specialized agent capabilities for developer workflows.
Files
Tailwind v4 + shadcn/ui Production Stack
Production-tested: WordPress Auditor (https://wordpress-auditor.webfonts.workers.dev) Last Updated: 2025-11-09 Status: Production Ready ✅
---
⚠️ 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
pnpm add tailwindcss @tailwindcss/vite
pnpm add -D @types/node
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",
"baseColor": "slate", // Base color palette
"cssVariables": true,
"prefix": "" // No prefix for utility classes
}
}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
Custom Colors
Add new semantic colors:
: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</div>
Migration from v3
See reference/migration-guide.md for complete v3 → v4 migration steps.
Component Best Practices
1. Always use semantic tokens
<Button variant="destructive">Delete</Button> /* ✅ */
<Button className="bg-red-600">Delete</Button> /* ❌ */2. Use `cn()` for conditional styling
import { cn } from "@/lib/utils"
<div className={cn(
"base-class",
isActive && "active-class",
hasError && "error-class"
)} />3. Compose shadcn/ui components
<Dialog>
<DialogTrigger asChild>
<Button>Open</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Title</DialogTitle>
</DialogHeader>
</DialogContent>
</Dialog>---
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.553.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@types/node": "^24.10.0",
"@vitejs/plugin-react": "^5.1.0",
"vite": "^7.2.2",
"typescript": "~5.9.0",
"tw-animate-css": "^1.4.0"
}
}Animation Packages (Updated Nov 2025)
shadcn/ui has deprecated tailwindcss-animate in favor of tw-animate-css for Tailwind v4 compatibility.
✅ DO Install (v4-compatible):
pnpm add -D tw-animate-cssThen add to src/index.css:
@import "tailwindcss";
@import "tw-animate-css";❌ DO NOT Install:
npm install tailwindcss-animate # Deprecated - v3 onlyWhy: tw-animate-css is the official v4-compatible replacement for animations, required by shadcn/ui components.
Reference: https://ui.shadcn.com/docs/tailwind-v4
---
Tailwind v4 Plugins
Tailwind v4 supports official plugins using the @plugin directive in CSS.
Official Plugins (Tailwind Labs)
Typography Plugin - Style Markdown/CMS Content
When to use: Displaying blog posts, documentation, or any HTML from Markdown/CMS.
Installation:
pnpm add -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:
pnpm add -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:
❌ WRONG (v3 config file syntax):
// tailwind.config.js
module.exports = {
plugins: [require('@tailwindcss/typography')]
}❌ WRONG (@import instead of @plugin):
@import "@tailwindcss/typography"; /* Doesn't work */✅ CORRECT (v4 @plugin directive):
/* src/index.css */
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";---
Built-in Features (No Plugin Needed)
Container queries are built into Tailwind v4 core - no plugin needed:
<div className="@container">
<div className="@md:text-lg">
Responds to container width, not viewport
</div>
</div>❌ Don't install: @tailwindcss/container-queries (deprecated, now core feature)
---
Reference Documentation
For deeper understanding, see:
- architecture.md - Deep dive into the 4-step pattern
- dark-mode.md - Complete dark mode implementation
- common-gotchas.md - All the ways it can break (and fixes)
- migration-guide.md - Migrating hardcoded colors to CSS variables
---
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
---
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.14 + shadcn/ui latest (Oct 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
{
"name": "tailwind-v4-shadcn",
"description": "Set up Tailwind v4 with shadcn/ui using @theme inline pattern and CSS variable architecture. Four-step mandatory pattern: define CSS variables at root, map to Tailwind utilities, apply base styles, get automatic dark mode. Use when: initializing React projects with Tailwind v4, setting up shadcn/ui dark mode, or fixing colors not working, theme not applying, CSS variables broken, tw-animate-css errors, or migrating from v3.",
"version": "1.0.0",
"author": {
"name": "Jeremy Dawes",
"email": "jeremy@jezweb.net"
},
"license": "MIT",
"repository": "https://github.com/jezweb/claude-skills",
"keywords": []
}
Tailwind v4 + shadcn/ui Skill
Status: Production Ready ✅ Last Updated: 2025-11-09 Production Tested: WordPress Auditor (https://wordpress-auditor.webfonts.workers.dev)
---
Auto-Trigger Keywords
This skill should be invoked when user mentions ANY of:
Primary Triggers:
tailwind v4tailwind css v4shadcn/uishadcn uivite + react + tailwind@tailwindcss/vite
Secondary Triggers:
dark mode setuptheme providertheme switching@theme inlinecss variables not workingcolors not applyingtailwind utilities missing
Error-Based Triggers:
tw-animate-css(common error)@apply deprecateddark: variant not workingcolors all black/whitebg-primary doesn't work@import typography(wrong v4 syntax)require @tailwindcss/typography(v3 syntax in v4)prose class not working@plugin directive
---
What This Skill Does
Sets up production-ready Vite + React + Tailwind CSS v4 + shadcn/ui with:
✅ Correct v4 architecture - @theme inline pattern, no config file ✅ Dark mode - ThemeProvider with system/light/dark support ✅ Error prevention - Fixes tw-animate-css, duplicate @layer, @apply deprecation ✅ Semantic colors - Full color palette with proper CSS variables ✅ Path aliases - @/ imports configured ✅ TypeScript - Full type safety ✅ Templates* - Proven file templates ready to copy
---
Known Issues This Skill Prevents
| Issue | Why It Happens | How Skill Fixes It |
|---|---|---|
tw-animate-css import error | shadcn init adds non-existent import | Provides clean CSS template |
Duplicate @layer base | shadcn init adds second block with @apply | Single clean @layer block |
| Colors don't work | Missing @theme inline mapping | Complete mapping provided |
| Dark mode broken | No ThemeProvider or wrong setup | Full ThemeProvider template |
| Wrong config | tailwind.config.ts used for theme | Empty config, CSS-only theme |
| Double hsl() wrapping | Common pattern mistake | Correct variable usage |
| Wrong plugin syntax | Using @import or require() for plugins | Correct @plugin directive documented |
---
When to Use This Skill
✅ Use When:
- Starting a new Vite + React project with Tailwind v4
- Adding Tailwind v4 to existing Vite project
- Migrating from Tailwind v3 to v4
- Integrating shadcn/ui components
- Setting up dark mode with theme switching
- Debugging Tailwind v4 color/theme issues
- Need production-tested v4 patterns
❌ Don't Use When:
- Using Tailwind v3 (different architecture)
- Using Next.js (different setup, use Next.js skill instead)
- Using PostCSS instead of Vite plugin
- Building pure CSS library (no React needed)
- User specifically requests manual setup for learning
---
Template Structure
~/.claude/skills/tailwind-v4-shadcn/
├── README.md # This file - auto-trigger keywords
├── SKILL.md # Complete documentation (623 lines)
├── templates/ # Ready-to-copy file templates
│ ├── index.css # v4 CSS architecture
│ ├── components.json # shadcn/ui v4 config
│ ├── vite.config.ts # Vite + Tailwind plugin
│ ├── tsconfig.app.json # TypeScript with aliases
│ ├── theme-provider.tsx # Dark mode provider
│ └── utils.ts # cn() utility
└── reference/ # Deep-dive docs
├── architecture.md
├── dark-mode.md
├── common-gotchas.md
└── migration-guide.md---
Quick Usage
When Claude detects trigger keywords, it should:
1. Confirm with user: "I found the tailwind-v4-shadcn skill. Use it?" 2. Explain benefits: "This prevents tw-animate-css errors and includes dark mode" 3. Use templates: Copy from templates/ directory 4. Follow SKILL.md: Complete step-by-step in SKILL.md 5. Verify: Test dev server, check dark mode toggle
---
Token Efficiency
| Approach | Tokens Used | Errors |
|---|---|---|
| Manual setup (no skill) | ~65,000 | 2-3 common errors |
| With this skill | ~20,000 | 0 (prevented) |
| Savings | ~70% | 100% reduction |
---
Dependencies Installed
{
"dependencies": {
"tailwindcss": "^4.1.17",
"@tailwindcss/vite": "^4.1.17",
"clsx": "^2.1.1",
"tailwind-merge": "^3.3.1",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@types/node": "^24.10.0",
"@vitejs/plugin-react": "^5.1.0",
"vite": "^7.2.2",
"typescript": "~5.9.0",
"tw-animate-css": "^1.4.0"
}
}---
Example Skill Invocation
User: "Set up a new Vite + React project with Tailwind v4"
↓
Claude: [Checks ~/.claude/skills/tailwind-v4-shadcn/]
↓
Claude: "I found the tailwind-v4-shadcn skill. Use it?
(Prevents tw-animate-css error, includes dark mode)"
↓
User: "Yes"
↓
Claude: [Uses templates/ + follows SKILL.md]
↓
Result: Working project in ~1 minute, 0 errors---
Skill Metadata
name: tailwind-v4-shadcn
version: 1.0.0
category: frontend-setup
stack: [vite, react, tailwind-v4, shadcn-ui]
confidence: high # Production-tested pattern
auto_invoke_threshold: 0.7 # Invoke if 70%+ match
maintained_by: jeremy@jezweb.net
last_tested: 2025-10-20---
Related Skills
react-vite-base- Vite + React without Tailwindcloudflare-react-full-stack- Adds Cloudflare Workersreact-form-zod- React Hook Form + Zod validation
---
Support
- Full Documentation: See
SKILL.md(623 lines) - Troubleshooting: See
reference/common-gotchas.md - Official Docs: https://ui.shadcn.com/docs/tailwind-v4
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
.darkfor dark mode overrides (NOT.dark { @theme { } }) - ❌ Never put
:rootor.darkinside@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.tsfor colors @theme inlinegenerates utility classes (bg-background,text-foreground)- Without this, utilities like
bg-primarywon'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
.darkclass 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
- shadcn/ui Tailwind v4 Guide: https://ui.shadcn.com/docs/tailwind-v4
- Tailwind v4 Docs: https://tailwindcss.com/docs
- shadcn/ui Theming: https://ui.shadcn.com/docs/theming
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!
{
"$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')
}
}
})