dark-mode-implementer

Dark Mode Implementer

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "dark-mode-implementer" with this command: npx skills add monkey1sai/openai-cli/monkey1sai-openai-cli-dark-mode-implementer

Dark Mode Implementer

Build robust dark/light mode theming with system preference detection and persistent storage.

Core Workflow

  • Choose strategy: CSS-only, Tailwind, or React context

  • Define color tokens: Create semantic color variables

  • Implement toggle: Add theme switch component

  • Detect system preference: Respect prefers-color-scheme

  • Persist choice: Store preference in localStorage

  • Prevent flash: Handle initial load correctly

Strategy Comparison

Strategy Best For Complexity

Tailwind class

React/Vue/Svelte apps Low

CSS media

Simple static sites Very Low

CSS Variables + JS Framework-agnostic Medium

React Context Complex React apps Medium

Tailwind CSS Dark Mode

Enable Class Strategy

// tailwind.config.js module.exports = { darkMode: 'class', // or 'media' for system-only theme: { extend: { colors: { // Semantic color tokens background: 'hsl(var(--background))', foreground: 'hsl(var(--foreground))', primary: 'hsl(var(--primary))', muted: 'hsl(var(--muted))', }, }, }, };

CSS Variables Setup

/* globals.css */ @tailwind base; @tailwind components; @tailwind utilities;

@layer base { :root { --background: 0 0% 100%; --foreground: 222 47% 11%; --primary: 221 83% 53%; --primary-foreground: 210 40% 98%; --secondary: 210 40% 96%; --secondary-foreground: 222 47% 11%; --muted: 210 40% 96%; --muted-foreground: 215 16% 47%; --accent: 210 40% 96%; --accent-foreground: 222 47% 11%; --destructive: 0 84% 60%; --destructive-foreground: 210 40% 98%; --border: 214 32% 91%; --input: 214 32% 91%; --ring: 221 83% 53%; --radius: 0.5rem; }

.dark { --background: 222 47% 11%; --foreground: 210 40% 98%; --primary: 217 91% 60%; --primary-foreground: 222 47% 11%; --secondary: 217 33% 17%; --secondary-foreground: 210 40% 98%; --muted: 217 33% 17%; --muted-foreground: 215 20% 65%; --accent: 217 33% 17%; --accent-foreground: 210 40% 98%; --destructive: 0 62% 30%; --destructive-foreground: 210 40% 98%; --border: 217 33% 17%; --input: 217 33% 17%; --ring: 224 76% 48%; } }

@layer base {

  • { @apply border-border; } body { @apply bg-background text-foreground; } }

Using Dark Mode Classes

<!-- Component with dark mode variants --> <div class="bg-white dark:bg-gray-900"> <h1 class="text-gray-900 dark:text-white">Title</h1> <p class="text-gray-600 dark:text-gray-300">Description</p> <button class="bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700"> Action </button> </div>

React Theme Provider

Complete Theme Context

// lib/theme-context.tsx 'use client';

import { createContext, useContext, useEffect, useState } from 'react';

type Theme = 'light' | 'dark' | 'system';

interface ThemeContextType { theme: Theme; setTheme: (theme: Theme) => void; resolvedTheme: 'light' | 'dark'; }

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

const STORAGE_KEY = 'theme';

export function ThemeProvider({ children }: { children: React.ReactNode }) { const [theme, setThemeState] = useState<Theme>('system'); const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light'); const [mounted, setMounted] = useState(false);

// Get system preference const getSystemTheme = (): 'light' | 'dark' => { if (typeof window === 'undefined') return 'light'; return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; };

// Apply theme to document const applyTheme = (theme: Theme) => { const root = document.documentElement; const resolved = theme === 'system' ? getSystemTheme() : theme;

root.classList.remove('light', 'dark');
root.classList.add(resolved);
setResolvedTheme(resolved);

};

// Set theme and persist const setTheme = (newTheme: Theme) => { setThemeState(newTheme); localStorage.setItem(STORAGE_KEY, newTheme); applyTheme(newTheme); };

// Initialize theme on mount useEffect(() => { const stored = localStorage.getItem(STORAGE_KEY) as Theme | null; const initialTheme = stored || 'system'; setThemeState(initialTheme); applyTheme(initialTheme); setMounted(true); }, []);

// Listen for system preference changes useEffect(() => { const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');

const handleChange = () => {
  if (theme === 'system') {
    applyTheme('system');
  }
};

mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);

}, [theme]);

// Prevent hydration mismatch if (!mounted) { return <>{children}</>; }

return ( <ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}> {children} </ThemeContext.Provider> ); }

export function useTheme() { const context = useContext(ThemeContext); if (!context) { throw new Error('useTheme must be used within a ThemeProvider'); } return context; }

Prevent Flash of Wrong Theme

// app/layout.tsx import { ThemeProvider } from '@/lib/theme-context';

// Inline script to prevent flash const themeScript = (function() { const stored = localStorage.getItem('theme'); const theme = stored || 'system'; const resolved = theme === 'system' ? window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' : theme; document.documentElement.classList.add(resolved); })();;

export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en" suppressHydrationWarning> <head> <script dangerouslySetInnerHTML={{ __html: themeScript }} /> </head> <body> <ThemeProvider>{children}</ThemeProvider> </body> </html> ); }

Theme Toggle Components

Simple Toggle Button

// components/ThemeToggle.tsx 'use client';

import { useTheme } from '@/lib/theme-context'; import { Moon, Sun } from 'lucide-react';

export function ThemeToggle() { const { resolvedTheme, setTheme } = useTheme();

return ( <button onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')} className="rounded-lg p-2 hover:bg-gray-100 dark:hover:bg-gray-800" aria-label={Switch to ${resolvedTheme === 'dark' ? 'light' : 'dark'} mode} > {resolvedTheme === 'dark' ? ( <Sun className="h-5 w-5" /> ) : ( <Moon className="h-5 w-5" /> )} </button> ); }

Three-Way Toggle (Light/Dark/System)

// components/ThemeSelector.tsx 'use client';

import { useTheme } from '@/lib/theme-context'; import { Monitor, Moon, Sun } from 'lucide-react';

const themes = [ { value: 'light', icon: Sun, label: 'Light' }, { value: 'dark', icon: Moon, label: 'Dark' }, { value: 'system', icon: Monitor, label: 'System' }, ] as const;

export function ThemeSelector() { const { theme, setTheme } = useTheme();

return ( <div className="flex rounded-lg bg-gray-100 p-1 dark:bg-gray-800"> {themes.map(({ value, icon: Icon, label }) => ( <button key={value} onClick={() => setTheme(value)} className={ flex items-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${theme === value ? 'bg-white text-gray-900 shadow dark:bg-gray-700 dark:text-white' : 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white' } } aria-label={Switch to ${label} theme} > <Icon className="h-4 w-4" /> <span className="hidden sm:inline">{label}</span> </button> ))} </div> ); }

Animated Toggle Switch

// components/ThemeSwitch.tsx 'use client';

import { useTheme } from '@/lib/theme-context'; import { motion } from 'framer-motion';

export function ThemeSwitch() { const { resolvedTheme, setTheme } = useTheme(); const isDark = resolvedTheme === 'dark';

return ( <button onClick={() => setTheme(isDark ? 'light' : 'dark')} className="relative h-8 w-14 rounded-full bg-gray-200 p-1 dark:bg-gray-700" aria-label={Switch to ${isDark ? 'light' : 'dark'} mode} > <motion.div className="flex h-6 w-6 items-center justify-center rounded-full bg-white shadow-md" animate={{ x: isDark ? 24 : 0 }} transition={{ type: 'spring', stiffness: 500, damping: 30 }} > <motion.span initial={false} animate={{ rotate: isDark ? 360 : 0 }} transition={{ duration: 0.5 }} > {isDark ? '🌙' : '☀️'} </motion.span> </motion.div> </button> ); }

CSS-Only Dark Mode

Using prefers-color-scheme

/* For simple sites without JavaScript */ :root { --bg: #ffffff; --text: #1a1a1a; --primary: #3b82f6; }

@media (prefers-color-scheme: dark) { :root { --bg: #0a0a0a; --text: #fafafa; --primary: #60a5fa; } }

body { background-color: var(--bg); color: var(--text); }

Color Token System

Semantic Color Naming

:root { /* Background colors */ --color-bg-primary: #ffffff; --color-bg-secondary: #f9fafb; --color-bg-tertiary: #f3f4f6; --color-bg-inverse: #111827;

/* Text colors */ --color-text-primary: #111827; --color-text-secondary: #4b5563; --color-text-tertiary: #9ca3af; --color-text-inverse: #ffffff;

/* Border colors */ --color-border-primary: #e5e7eb; --color-border-secondary: #d1d5db;

/* Interactive colors */ --color-interactive-primary: #3b82f6; --color-interactive-hover: #2563eb; --color-interactive-active: #1d4ed8;

/* Status colors */ --color-success: #10b981; --color-warning: #f59e0b; --color-error: #ef4444; --color-info: #3b82f6; }

.dark { --color-bg-primary: #0f172a; --color-bg-secondary: #1e293b; --color-bg-tertiary: #334155; --color-bg-inverse: #f8fafc;

--color-text-primary: #f8fafc; --color-text-secondary: #cbd5e1; --color-text-tertiary: #64748b; --color-text-inverse: #0f172a;

--color-border-primary: #334155; --color-border-secondary: #475569;

--color-interactive-primary: #60a5fa; --color-interactive-hover: #3b82f6; --color-interactive-active: #2563eb;

--color-success: #34d399; --color-warning: #fbbf24; --color-error: #f87171; --color-info: #60a5fa; }

Next.js with next-themes

Installation and Setup

npm install next-themes

// app/providers.tsx 'use client';

import { ThemeProvider } from 'next-themes';

export function Providers({ children }: { children: React.ReactNode }) { return ( <ThemeProvider attribute="class" defaultTheme="system" enableSystem> {children} </ThemeProvider> ); }

// app/layout.tsx import { Providers } from './providers';

export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en" suppressHydrationWarning> <body> <Providers>{children}</Providers> </body> </html> ); }

// components/ThemeToggle.tsx 'use client';

import { useTheme } from 'next-themes'; import { useEffect, useState } from 'react';

export function ThemeToggle() { const { theme, setTheme } = useTheme(); const [mounted, setMounted] = useState(false);

useEffect(() => setMounted(true), []);

if (!mounted) return null;

return ( <select value={theme} onChange={(e) => setTheme(e.target.value)}> <option value="system">System</option> <option value="light">Light</option> <option value="dark">Dark</option> </select> ); }

Images and Media

Theme-Aware Images

// Swap images based on theme <picture> <source srcSet="/dark-logo.svg" media="(prefers-color-scheme: dark)" /> <img src="/light-logo.svg" alt="Logo" /> </picture>

// With Tailwind <img src="/light-logo.svg" alt="Logo" class="dark:hidden" /> <img src="/dark-logo.svg" alt="Logo" class="hidden dark:block" />

SVG Color Adaptation

// SVG that adapts to theme <svg className="text-gray-900 dark:text-white" fill="currentColor"> {/* ... */} </svg>

Testing Dark Mode

Playwright Test

// tests/dark-mode.spec.ts import { test, expect } from '@playwright/test';

test.describe('Dark Mode', () => { test('respects system preference', async ({ page }) => { await page.emulateMedia({ colorScheme: 'dark' }); await page.goto('/');

const html = page.locator('html');
await expect(html).toHaveClass(/dark/);

});

test('toggles theme correctly', async ({ page }) => { await page.goto('/');

await page.click('[aria-label*="dark"]');
await expect(page.locator('html')).toHaveClass(/dark/);

await page.click('[aria-label*="light"]');
await expect(page.locator('html')).not.toHaveClass(/dark/);

});

test('persists theme preference', async ({ page }) => { await page.goto('/'); await page.click('[aria-label*="dark"]');

await page.reload();
await expect(page.locator('html')).toHaveClass(/dark/);

}); });

Best Practices

  • Prevent flash: Use inline script before React hydration

  • Respect system preference: Default to 'system' theme

  • Persist choice: Store in localStorage

  • Use semantic tokens: Don't hardcode colors

  • Test both themes: Ensure contrast and readability

  • Handle images: Provide dark-mode variants

  • Consider transitions: Add smooth color transitions

  • Accessibility: Maintain WCAG contrast ratios

Output Checklist

Every dark mode implementation should include:

  • Tailwind configured with darkMode: 'class'

  • CSS variables for light and dark themes

  • Theme provider with context

  • System preference detection

  • localStorage persistence

  • Flash prevention script

  • Theme toggle component

  • Accessible labels on toggle

  • Color tokens for all UI elements

  • Dark variants for images/SVGs

  • Smooth transition animations

  • Playwright/Jest tests

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

responsive-design-system

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

rate-limiting-abuse-protection

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

bruno-collection-generator

No summary provided by upstream source.

Repository SourceNeeds Review