Tailwind CSS Dark Mode: Complete Implementation Guide
ID | EN

Tailwind CSS Dark Mode: Complete Implementation Guide

Kamis, 16 Jan 2025

Dark mode has become an essential feature for modern web applications. Tailwind CSS makes implementing dark mode straightforward with built-in utilities and flexible configuration options. This guide covers everything you need to know about Tailwind CSS dark mode implementation.

Understanding Tailwind Dark Mode Strategies

Tailwind CSS offers two strategies for dark mode: media and class. Each approach has distinct use cases and trade-offs.

Media Strategy (Default)

The media strategy uses the prefers-color-scheme CSS media query to automatically detect the user’s system preference:

// tailwind.config.js
module.exports = {
  darkMode: 'media',
  // ...
}

With this configuration, dark mode activates automatically when the user’s operating system is set to dark mode. No JavaScript is required.

Class Strategy

The class strategy gives you manual control over dark mode by toggling a dark class on the HTML element:

// tailwind.config.js
module.exports = {
  darkMode: 'class',
  // ...
}

This approach is ideal when you want users to override their system preference or when building applications that need programmatic theme control.

Using the dark: Variant

Once configured, apply dark mode styles using the dark: variant prefix:

<div class="bg-white dark:bg-gray-900">
  <h1 class="text-gray-900 dark:text-white">
    Welcome to Dark Mode
  </h1>
  <p class="text-gray-600 dark:text-gray-300">
    This text adapts to the current theme.
  </p>
</div>

The dark: variant works with any Tailwind utility class, making it simple to define both light and dark styles inline.

Combining with Other Variants

Dark mode variants can be combined with responsive breakpoints and state variants:

<button class="
  bg-blue-500 hover:bg-blue-600
  dark:bg-blue-600 dark:hover:bg-blue-700
  md:px-6 md:py-3
">
  Click Me
</button>

System Preference Detection

For the class strategy, you’ll want to detect the user’s system preference on page load. Add this script to your HTML <head> to prevent flash of unstyled content (FOUC):

<script>
  if (localStorage.theme === 'dark' || 
      (!('theme' in localStorage) && 
       window.matchMedia('(prefers-color-scheme: dark)').matches)) {
    document.documentElement.classList.add('dark')
  } else {
    document.documentElement.classList.remove('dark')
  }
</script>

This script runs before the page renders, checking localStorage first, then falling back to the system preference.

Manual Toggle with localStorage Persistence

Create a theme toggle that remembers user preferences across sessions:

// theme-toggle.js
function toggleTheme() {
  const html = document.documentElement;
  
  if (html.classList.contains('dark')) {
    html.classList.remove('dark');
    localStorage.theme = 'light';
  } else {
    html.classList.add('dark');
    localStorage.theme = 'dark';
  }
}

function setTheme(theme) {
  const html = document.documentElement;
  
  if (theme === 'dark') {
    html.classList.add('dark');
    localStorage.theme = 'dark';
  } else if (theme === 'light') {
    html.classList.remove('dark');
    localStorage.theme = 'light';
  } else {
    // System preference
    localStorage.removeItem('theme');
    if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
      html.classList.add('dark');
    } else {
      html.classList.remove('dark');
    }
  }
}

Toggle Button Component

Here’s a simple toggle button implementation:

<button 
  onclick="toggleTheme()"
  class="p-2 rounded-lg bg-gray-200 dark:bg-gray-700"
  aria-label="Toggle dark mode"
>
  <!-- Sun icon (shown in dark mode) -->
  <svg class="hidden dark:block w-5 h-5 text-yellow-500" fill="currentColor" viewBox="0 0 20 20">
    <path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/>
  </svg>
  <!-- Moon icon (shown in light mode) -->
  <svg class="block dark:hidden w-5 h-5 text-gray-700" fill="currentColor" viewBox="0 0 20 20">
    <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
  </svg>
</button>

React and Next.js Integration with next-themes

For React and Next.js applications, the next-themes library provides an elegant solution with built-in SSR support:

npm install next-themes

Setting Up the Theme Provider

Wrap your application with the ThemeProvider:

// app/providers.jsx
'use client'

import { ThemeProvider } from 'next-themes'

export function Providers({ children }) {
  return (
    <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
      {children}
    </ThemeProvider>
  )
}
// app/layout.jsx
import { Providers } from './providers'

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

Creating a Theme Toggle Component

'use client'

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

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

  useEffect(() => {
    setMounted(true)
  }, [])

  if (!mounted) {
    return <div className="w-9 h-9" /> // Placeholder to prevent layout shift
  }

  return (
    <button
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
      className="p-2 rounded-lg bg-gray-200 dark:bg-gray-700 
                 hover:bg-gray-300 dark:hover:bg-gray-600 
                 transition-colors"
      aria-label="Toggle theme"
    >
      {theme === 'dark' ? '☀️' : '🌙'}
    </button>
  )
}

Theme Selector with System Option

'use client'

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

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

  useEffect(() => {
    setMounted(true)
  }, [])

  if (!mounted) return null

  return (
    <select
      value={theme}
      onChange={(e) => setTheme(e.target.value)}
      className="px-3 py-2 rounded-lg border border-gray-300 
                 dark:border-gray-600 bg-white dark:bg-gray-800
                 text-gray-900 dark:text-white"
    >
      <option value="system">System</option>
      <option value="light">Light</option>
      <option value="dark">Dark</option>
    </select>
  )
}

Best Practices for Dark Mode

1. Design with Both Modes in Mind

Plan your color palette for both light and dark modes from the start. Use Tailwind’s color scale consistently:

<!-- Good: Consistent color relationships -->
<div class="bg-gray-50 dark:bg-gray-900">
  <p class="text-gray-700 dark:text-gray-300">Content</p>
</div>

<!-- Avoid: Inconsistent jumps in color scale -->
<div class="bg-white dark:bg-black">
  <p class="text-gray-900 dark:text-gray-100">Content</p>
</div>

2. Use CSS Custom Properties for Complex Themes

For applications with multiple themes or complex color requirements, combine Tailwind with CSS custom properties:

/* globals.css */
:root {
  --color-primary: 59 130 246;
  --color-background: 255 255 255;
  --color-text: 17 24 39;
}

.dark {
  --color-primary: 96 165 250;
  --color-background: 17 24 39;
  --color-text: 243 244 246;
}
// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        primary: 'rgb(var(--color-primary) / <alpha-value>)',
        background: 'rgb(var(--color-background) / <alpha-value>)',
        text: 'rgb(var(--color-text) / <alpha-value>)',
      },
    },
  },
}

3. Handle Images and Media

Adjust images for dark mode using opacity or filters:

<!-- Reduce brightness in dark mode -->
<img 
  src="/hero.jpg" 
  class="dark:brightness-90 dark:contrast-105"
  alt="Hero image"
/>

<!-- Show different images per theme -->
<img 
  src="/logo-light.svg" 
  class="dark:hidden"
  alt="Logo"
/>
<img 
  src="/logo-dark.svg" 
  class="hidden dark:block"
  alt="Logo"
/>

4. Consider Accessibility

Maintain sufficient contrast ratios in both modes. Use Tailwind’s opacity modifiers to fine-tune:

<p class="text-gray-600 dark:text-gray-400">
  <!-- May have low contrast in dark mode -->
</p>

<p class="text-gray-600 dark:text-gray-300">
  <!-- Better contrast in dark mode -->
</p>

5. Prevent Flash of Incorrect Theme

Always include the theme detection script in the <head> before any stylesheets load. For Next.js with next-themes, add suppressHydrationWarning to the <html> element.

Common Patterns

Card Component

<div class="
  bg-white dark:bg-gray-800 
  border border-gray-200 dark:border-gray-700
  rounded-xl shadow-sm dark:shadow-gray-900/20
  p-6
">
  <h3 class="text-lg font-semibold text-gray-900 dark:text-white">
    Card Title
  </h3>
  <p class="mt-2 text-gray-600 dark:text-gray-400">
    Card description goes here.
  </p>
</div>

Input Field

<input 
  type="text"
  placeholder="Enter text..."
  class="
    w-full px-4 py-2 rounded-lg
    bg-white dark:bg-gray-800
    border border-gray-300 dark:border-gray-600
    text-gray-900 dark:text-white
    placeholder-gray-500 dark:placeholder-gray-400
    focus:ring-2 focus:ring-blue-500 focus:border-transparent
    dark:focus:ring-blue-400
  "
/>
<nav class="
  bg-white dark:bg-gray-900 
  border-b border-gray-200 dark:border-gray-800
  sticky top-0 z-50
">
  <div class="max-w-7xl mx-auto px-4 py-4">
    <a href="/" class="text-gray-900 dark:text-white font-bold">
      Logo
    </a>
  </div>
</nav>

Conclusion

Tailwind CSS provides a robust foundation for implementing dark mode in your applications. Whether you choose the automatic media strategy or the flexible class strategy, Tailwind’s utility-first approach makes it easy to maintain consistent styling across both themes.

Start with the class strategy if you need user controls, integrate next-themes for React applications, and follow the best practices outlined above to create a polished dark mode experience that your users will appreciate.