---
name: setup-dark-mode
description: Set up comprehensive dark mode support with TailwindCSS using CSS variables, theme switching, and system preferences
tools: Write, Edit, Read, Bash
---
# Setup Dark Mode with TailwindCSS
This command sets up a complete dark mode system using TailwindCSS with CSS variables, automatic theme detection, and smooth transitions.
## What This Command Does
1. **CSS Variables Configuration**
- Sets up semantic color system using CSS variables
- Configures light and dark theme variants
- Creates smooth transition system between themes
- Implements proper contrast ratios for accessibility
2. **Theme Configuration**
- Configures TailwindCSS for class-based dark mode
- Sets up color palette using CSS variables
- Creates theme-aware utility classes
- Optimizes for design system consistency
3. **JavaScript Theme Controller**
- Detects system theme preferences
- Provides manual theme switching functionality
- Persists user theme preferences
- Handles theme transitions smoothly
4. **Component Integration**
- Creates theme-aware components
- Implements proper dark mode patterns
- Sets up theme toggle components
- Provides theme context for React/Vue apps
## Configuration Setup
### TailwindCSS Configuration
```javascript
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./src/**/*.{js,ts,jsx,tsx,mdx}',
],
darkMode: 'class', // Enable class-based dark mode
theme: {
extend: {
colors: {
// CSS variable-based color system
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
// Semantic colors
success: {
DEFAULT: 'hsl(var(--success))',
foreground: 'hsl(var(--success-foreground))',
},
warning: {
DEFAULT: 'hsl(var(--warning))',
foreground: 'hsl(var(--warning-foreground))',
},
info: {
DEFAULT: 'hsl(var(--info))',
foreground: 'hsl(var(--info-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
boxShadow: {
'sm': 'var(--shadow-sm)',
'DEFAULT': 'var(--shadow)',
'md': 'var(--shadow-md)',
'lg': 'var(--shadow-lg)',
'xl': 'var(--shadow-xl)',
},
},
},
plugins: [],
}
```
### CSS Variables Setup
```css
/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* Light theme colors */
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
/* Semantic colors */
--success: 142.1 76.2% 36.3%;
--success-foreground: 355.7 100% 97.3%;
--warning: 32.5 94.6% 43.7%;
--warning-foreground: 26 83.3% 14.1%;
--info: 217.2 91.2% 59.8%;
--info-foreground: 210 40% 98%;
/* Design tokens */
--radius: 0.5rem;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
}
.dark {
/* Dark theme colors */
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 84% 4.9%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
/* Semantic colors for dark theme */
--success: 142.1 70.6% 45.3%;
--success-foreground: 144.9 80.4% 10%;
--warning: 32.5 94.6% 43.7%;
--warning-foreground: 26 83.3% 14.1%;
--info: 217.2 91.2% 59.8%;
--info-foreground: 222.2 84% 4.9%;
/* Dark theme shadows */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.4), 0 1px 2px -1px rgb(0 0 0 / 0.3);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.3);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.4), 0 8px 10px -6px rgb(0 0 0 / 0.3);
}
/* Global base styles */
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
}
/* Smooth theme transitions */
html {
transition: color-scheme 0.2s ease-in-out;
}
* {
transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out, color 0.2s ease-in-out;
}
/* Focus styles */
.focus-visible {
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2;
}
}
/* Custom scrollbar for dark mode */
@layer utilities {
.scrollbar-thin {
scrollbar-width: thin;
}
.scrollbar-track-transparent {
scrollbar-color: hsl(var(--muted)) transparent;
}
.dark .scrollbar-track-transparent {
scrollbar-color: hsl(var(--muted)) transparent;
}
}
```
## Theme Management
### JavaScript Theme Controller
```javascript
// lib/theme.js
class ThemeManager {
constructor() {
this.theme = 'system'
this.systemTheme = 'light'
this.init()
}
init() {
// Get stored theme or default to system
this.theme = localStorage.getItem('theme') || 'system'
// Listen for system theme changes
this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
this.systemTheme = this.mediaQuery.matches ? 'dark' : 'light'
this.mediaQuery.addEventListener('change', (e) => {
this.systemTheme = e.matches ? 'dark' : 'light'
if (this.theme === 'system') {
this.applyTheme()
}
})
// Apply initial theme
this.applyTheme()
}
setTheme(theme) {
this.theme = theme
localStorage.setItem('theme', theme)
this.applyTheme()
this.notifyListeners()
}
applyTheme() {
const root = document.documentElement
const isDark = this.theme === 'dark' || (this.theme === 'system' && this.systemTheme === 'dark')
if (isDark) {
root.classList.add('dark')
root.style.colorScheme = 'dark'
} else {
root.classList.remove('dark')
root.style.colorScheme = 'light'
}
}
getTheme() {
return this.theme
}
getEffectiveTheme() {
return this.theme === 'system' ? this.systemTheme : this.theme
}
// Event listener system
listeners = new Set()
subscribe(callback) {
this.listeners.add(callback)
return () => this.listeners.delete(callback)
}
notifyListeners() {
this.listeners.forEach(callback => {
callback({
theme: this.theme,
effectiveTheme: this.getEffectiveTheme()
})
})
}
}
// Create global instance
const themeManager = new ThemeManager()
export { themeManager }
```
### React Theme Hook
```jsx
// hooks/useTheme.js
import { useState, useEffect } from 'react'
import { themeManager } from '@/lib/theme'
export function useTheme() {
const [theme, setThemeState] = useState(themeManager.getTheme())
const [effectiveTheme, setEffectiveTheme] = useState(themeManager.getEffectiveTheme())
useEffect(() => {
const unsubscribe = themeManager.subscribe(({ theme, effectiveTheme }) => {
setThemeState(theme)
setEffectiveTheme(effectiveTheme)
})
return unsubscribe
}, [])
const setTheme = (newTheme) => {
themeManager.setTheme(newTheme)
}
return {
theme,
effectiveTheme,
setTheme,
themes: ['light', 'dark', 'system']
}
}
```
### React Theme Provider
```jsx
// providers/ThemeProvider.jsx
import React, { createContext, useContext, useEffect, useState } from 'react'
const ThemeProviderContext = createContext({
theme: 'system',
setTheme: () => null,
})
export function ThemeProvider({ children, defaultTheme = 'system' }) {
const [theme, setTheme] = useState(() => {
if (typeof window !== 'undefined') {
return localStorage.getItem('theme') || defaultTheme
}
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) => {
localStorage.setItem('theme', theme)
setTheme(theme)
},
}
return (
{children}
)
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
if (context === undefined)
throw new Error('useTheme must be used within a ThemeProvider')
return context
}
```
## Theme Toggle Components
### Simple Theme Toggle
```jsx
// components/ThemeToggle.jsx
import React from 'react'
import { Moon, Sun } from 'lucide-react'
import { useTheme } from '@/hooks/useTheme'
import { Button } from '@/components/ui/Button'
export function ThemeToggle() {
const { effectiveTheme, setTheme } = useTheme()
const toggleTheme = () => {
setTheme(effectiveTheme === 'light' ? 'dark' : 'light')
}
return (
)
}
```
### Advanced Theme Selector
```jsx
// components/ThemeSelector.jsx
import React from 'react'
import { Monitor, Moon, Sun } from 'lucide-react'
import { useTheme } from '@/hooks/useTheme'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/DropdownMenu'
import { Button } from '@/components/ui/Button'
export function ThemeSelector() {
const { theme, setTheme } = useTheme()
const themes = [
{ value: 'light', label: 'Light', icon: Sun },
{ value: 'dark', label: 'Dark', icon: Moon },
{ value: 'system', label: 'System', icon: Monitor },
]
const currentTheme = themes.find(t => t.value === theme)
return (
{themes.map(({ value, label, icon: Icon }) => (
setTheme(value)}
className="cursor-pointer"
>
{label}
{theme === value && (
✓
)}
))}
)
}
```
### Animated Theme Toggle
```jsx
// components/AnimatedThemeToggle.jsx
import React from 'react'
import { useTheme } from '@/hooks/useTheme'
import { cn } from '@/lib/utils'
export function AnimatedThemeToggle() {
const { effectiveTheme, setTheme } = useTheme()
const isDark = effectiveTheme === 'dark'
const toggleTheme = () => {
setTheme(isDark ? 'light' : 'dark')
}
return (
)
}
```
## Theme-Aware Components
### Dark Mode Image Component
```jsx
// components/ThemeAwareImage.jsx
import React from 'react'
import { useTheme } from '@/hooks/useTheme'
export function ThemeAwareImage({
lightSrc,
darkSrc,
alt,
className,
...props
}) {
const { effectiveTheme } = useTheme()
const src = effectiveTheme === 'dark' ? darkSrc : lightSrc
return (
)
}
```
### Theme Detection Script
```html
```
## Testing Dark Mode
### Dark Mode Test Suite
```javascript
// tests/dark-mode.test.js
import { render, screen, fireEvent } from '@testing-library/react'
import { ThemeProvider } from '@/providers/ThemeProvider'
import { ThemeToggle } from '@/components/ThemeToggle'
describe('Dark Mode', () => {
beforeEach(() => {
localStorage.clear()
document.documentElement.className = ''
})
test('applies dark mode class when theme is dark', () => {
render(
Test content
)
expect(document.documentElement).toHaveClass('dark')
})
test('toggles theme when button is clicked', () => {
render(
)
const toggleButton = screen.getByLabelText(/toggle theme/i)
fireEvent.click(toggleButton)
expect(document.documentElement).toHaveClass('dark')
})
test('persists theme preference', () => {
render(
)
const toggleButton = screen.getByLabelText(/toggle theme/i)
fireEvent.click(toggleButton)
expect(localStorage.getItem('theme')).toBe('dark')
})
})
```
Remember: **Dark mode should enhance user experience with proper contrast ratios, smooth transitions, and respect for user preferences!**