Back to Blog
Next.jsTypeScriptWeb DevelopmentPortfolioTailwind CSS

Setting Tailwind v4 dark theme for Claude Code

Setting up a modern Tailwind v4 stack with Next.js.

Kaf's profile picture
Kaf
Developer & Creator
6 min read

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 to document.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 and accent-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:

  1. Add dark class to document element in React
  2. Ensure CSS variables are properly defined in .dark selector
  3. 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:

  1. Remove tailwind.config.js
  2. Move color definitions to @theme in CSS
  3. Update PostCSS config to use import syntax
  4. Test all custom utilities and components

Best Practices

  1. Use semantic color names (bg-background not bg-gray-900)
  2. Define custom colors in @theme for brand consistency
  3. Test both light and dark themes even if you only use one
  4. Use oklch() color space for better color consistency
  5. Document your color system for team collaboration

Resources


This guide is based on successful implementations across React applications using various build tools.