Setting Tailwind v4 dark theme for Claude Code
This guide walks through setting up Tailwind CSS v4 with a dark theme in React applications using Vite or Next.js, based on a successful implementation.
Overview
Tailwind v4 introduces a CSS-first configuration approach, moving away from JavaScript config files to CSS-based theme definitions using the @theme
directive. This guide covers the complete setup including PostCSS configuration, dark theme implementation, and troubleshooting common issues.
Prerequisites
- React application (Vite, Next.js, or Create React App)
- Node.js and npm installed
- Basic understanding of CSS custom properties
Step 1: Install Dependencies
npm install tailwindcss @tailwindcss/postcss
Step 2: Create PostCSS Configuration
Create postcss.config.mjs
in your project root:
import tailwindcss from "@tailwindcss/postcss";
const config = {
plugins: [tailwindcss],
};
export default config;
⚠️ Critical Note: Use the import
syntax, not string references. The string syntax ["@tailwindcss/postcss"]
will cause "Invalid PostCSS Plugin" errors in Vite. Next.js users can use either syntax.
Step 3: CSS-First Configuration
Replace your main CSS file (src/index.css
for Vite/CRA, src/app/globals.css
for Next.js) with Tailwind v4 CSS-first configuration:
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
/* Tailwind v4 CSS-first configuration */
@theme {
--color-accent-blue: #60a5fa;
--color-accent-pink: #f472b6;
--color-background: #0a0a0a;
--color-foreground: #fafafa;
--color-card: #0a0a0a;
--color-card-foreground: #fafafa;
--color-popover: #0a0a0a;
--color-popover-foreground: #fafafa;
--color-primary: #fafafa;
--color-primary-foreground: #0a0a0a;
--color-secondary: #262626;
--color-secondary-foreground: #fafafa;
--color-muted: #262626;
--color-muted-foreground: #a3a3a3;
--color-accent: #262626;
--color-accent-foreground: #fafafa;
--color-destructive: #dc2626;
--color-destructive-foreground: #fafafa;
--color-border: #262626;
--color-input: #262626;
--color-ring: #a3a3a3;
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--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-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);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
Step 4: Enable Dark Theme in React
Add this to your main App component to force dark mode:
import { useEffect } from 'react';
function App() {
// Force dark mode by adding dark class to document element
useEffect(() => {
document.documentElement.classList.add('dark');
}, []);
return (
<div className="min-h-screen bg-background text-foreground">
{/* Your app content */}
</div>
);
}
Step 5: shadcn/ui Integration (Optional)
If using shadcn/ui components, add path aliases:
tsconfig.json:
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
vite.config.ts:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': resolve(__dirname, './src'),
},
},
});
Key Concepts
CSS-First Configuration
@theme
: Define custom colors and utilities directly in CSS- No JavaScript config: Tailwind v4 moves away from
tailwind.config.js
- CSS custom properties: All colors defined as
--color-name
format
Dark Theme Implementation
- CSS variables: Define light theme in
:root
, dark theme in.dark
- Document class: Must add
dark
class todocument.documentElement
- Custom variant:
@custom-variant dark (&:is(.dark *))
enables dark mode utilities
Color System
- Semantic naming:
background
,foreground
,primary
,secondary
, etc. - Consistent opacity: Use
oklch()
color space for better consistency - Custom accents: Define brand colors like
accent-blue
andaccent-pink
Troubleshooting
PostCSS Plugin Error
Error: "Invalid PostCSS Plugin found at: plugins[0]"
Solution: Use import syntax in postcss.config.mjs
:
// ✅ Correct
import tailwindcss from "@tailwindcss/postcss";
// ❌ Wrong - causes errors in Vite
plugins: ["@tailwindcss/postcss"]
Dark Theme Not Working
Problem: Theme stays light despite CSS configuration
Solution:
- Add
dark
class to document element in React - Ensure CSS variables are properly defined in
.dark
selector - Use semantic color classes like
bg-background text-foreground
Port Conflicts
Error: "Port 1420 is already in use"
Solution: npx kill-port 1420
before starting dev server
Testing Your Setup
Create a test component to verify everything works:
function ThemeTest() {
return (
<div className="bg-card border border-border rounded-lg p-6">
<h3 className="text-xl font-semibold mb-4 text-foreground">
Theme Test
</h3>
<div className="flex gap-2">
<div className="w-4 h-4 bg-accent-blue rounded"></div>
<div className="w-4 h-4 bg-accent-pink rounded"></div>
<div className="w-4 h-4 bg-primary rounded"></div>
<div className="w-4 h-4 bg-secondary rounded"></div>
</div>
<p className="text-muted-foreground mt-2">
Custom theme colors working ✅
</p>
</div>
);
}
Migration from Tailwind v3
If migrating from v3:
- Remove
tailwind.config.js
- Move color definitions to
@theme
in CSS - Update PostCSS config to use import syntax
- Test all custom utilities and components
Best Practices
- Use semantic color names (
bg-background
notbg-gray-900
) - Define custom colors in @theme for brand consistency
- Test both light and dark themes even if you only use one
- Use oklch() color space for better color consistency
- Document your color system for team collaboration
Resources
This guide is based on successful implementations across React applications using various build tools.