How to Build Dark Mode with CSS

Dark mode has become a standard feature users expect on modern websites. Besides looking elegant, dark mode also reduces eye strain when browsing at night. This article covers two...

How to Build Dark Mode with CSS

Dark mode has become a standard feature users expect on modern websites. Besides looking elegant, dark mode also reduces eye strain when browsing at night. This article covers two approaches to building dark mode: automatically following the operating system's preference, and manually with a toggle button the user can click.

The Basic Concept: CSS Variables for Theming

The key to a clean dark mode implementation is using CSS variables. Define all colors as variables, then change their values for the dark theme:

/* Light theme (default) */
:root {
  --color-bg: #ffffff;
  --color-bg-secondary: #f8fafc;
  --color-text: #1e293b;
  --color-text-muted: #64748b;
  --color-border: #e2e8f0;
  --color-card: #ffffff;
  --color-shadow: rgba(0, 0, 0, 0.08);
  --color-primary: #3b82f6;
  --color-primary-hover: #2563eb;
}

/* Dark theme */
[data-theme="dark"] {
  --color-bg: #0f172a;
  --color-bg-secondary: #1e293b;
  --color-text: #f1f5f9;
  --color-text-muted: #94a3b8;
  --color-border: #334155;
  --color-card: #1e293b;
  --color-shadow: rgba(0, 0, 0, 0.3);
  --color-primary: #60a5fa;
  --color-primary-hover: #93c5fd;
}

Applying the Variables to Elements

body {
  background-color: var(--color-bg);
  color: var(--color-text);
  transition: background-color 0.3s ease, color 0.3s ease;
  font-family: system-ui, sans-serif;
}

.card {
  background: var(--color-card);
  border: 1px solid var(--color-border);
  box-shadow: 0 2px 8px var(--color-shadow);
  border-radius: 12px;
  padding: 24px;
}

.text-muted {
  color: var(--color-text-muted);
}

.button {
  background: var(--color-primary);
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.2s;
}

.button:hover {
  background: var(--color-primary-hover);
}

input, textarea {
  background: var(--color-bg-secondary);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  padding: 10px 14px;
  border-radius: 8px;
  width: 100%;
}

Method 1: Automatic via prefers-color-scheme

Browsers and CSS support the prefers-color-scheme media query, which automatically detects the user's operating system theme preference:

/* Light theme (default) */
:root {
  --color-bg: #ffffff;
  --color-text: #1e293b;
  --color-card: #f8fafc;
  --color-border: #e2e8f0;
}

/* Automatically switch to dark if the OS uses dark mode */
@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #0f172a;
    --color-text: #f1f5f9;
    --color-card: #1e293b;
    --color-border: #334155;
  }
}

With this approach, no JavaScript is needed at all. The website automatically matches the theme to the user's system settings.

Method 2: Manual Toggle with JavaScript

Create a toggle button that changes the data-theme attribute on the <html> element:

<!-- HTML: Toggle Button -->
<button class="theme-toggle-btn" id="toggleTheme" aria-label="Toggle dark mode">
  <span id="themeIcon">☾</span> Dark Mode
</button>
<!-- CSS for the Toggle Button -->
<style>
.theme-toggle-btn {
  background: var(--color-bg-secondary);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  padding: 8px 16px;
  border-radius: 20px;
  cursor: pointer;
  font-size: 0.9rem;
  transition: all 0.2s;
}

.theme-toggle-btn:hover {
  border-color: var(--color-primary);
  color: var(--color-primary);
}
</style>
<!-- JavaScript -->
<script>
const toggleBtn = document.getElementById('toggleTheme');
const themeIcon = document.getElementById('themeIcon');
const html      = document.documentElement;

// Check the preference saved in localStorage
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
  html.setAttribute('data-theme', 'dark');
  themeIcon.textContent = '☀'; // sun icon
  toggleBtn.lastChild.textContent = ' Light Mode';
}

toggleBtn.addEventListener('click', function () {
  const currentTheme = html.getAttribute('data-theme');

  if (currentTheme === 'dark') {
    html.removeAttribute('data-theme');
    localStorage.setItem('theme', 'light');
    themeIcon.textContent = '☽'; // moon icon
    toggleBtn.lastChild.textContent = ' Dark Mode';
  } else {
    html.setAttribute('data-theme', 'dark');
    localStorage.setItem('theme', 'dark');
    themeIcon.textContent = '☀'; // sun icon
    toggleBtn.lastChild.textContent = ' Light Mode';
  }
});
</script>

Combining Both Approaches

The best approach is a combination of both: default to the system preference, but the user can still change it manually:

/* Detect the system preference by default */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --color-bg: #0f172a;
    --color-text: #f1f5f9;
    --color-card: #1e293b;
    --color-border: #334155;
  }
}

/* Manual dark mode */
[data-theme="dark"] {
  --color-bg: #0f172a;
  --color-text: #f1f5f9;
  --color-card: #1e293b;
  --color-border: #334155;
}

Smooth Transitions

Add a transition so the theme switch feels smooth and not abrupt:

body,
.card,
input,
textarea,
button {
  transition:
    background-color 0.3s ease,
    color 0.3s ease,
    border-color 0.3s ease;
}

Tips for a Good Dark Mode

  • Avoid pure black (#000000) for the background — use dark colors like #0f172a or #111827 that are more comfortable for the eyes.
  • Make sure the text and background contrast meets WCAG accessibility standards (at least 4.5:1 for normal text).
  • Save the user's preference in localStorage so the choice persists when the page is refreshed.
  • Don't forget to adjust images and icons too — consider using a CSS filter for images that are too bright in dark mode.
/* Optimize images in dark mode */
[data-theme="dark"] img:not([class*="logo"]) {
  filter: brightness(0.85) contrast(1.05);
}

Conclusion

Building dark mode with CSS isn't complicated if you use CSS variables as the foundation. The prefers-color-scheme approach makes the website automatically match the user's system theme, while a JavaScript toggle button gives the user full control. Save the preference in localStorage for a more consistent experience. With a well-implemented dark mode, your website will feel more modern, comfortable, and professional.

dark mode css dark mode website prefers-color-scheme css variable dark mode toggle dark mode tutorial dark mode
Share this article
Back to Blog
🚀 Partner Recommendation

Need Premium Source Code & Business Apps?

Access Laravel applications, POS systems, School Management, Clinic Software, ERP solutions, and ready-to-use premium source code at GudangCode.

GudangCode
  • ✔ Premium Source Code
  • ✔ Ready-to-Use Systems
  • ✔ Lifetime Updates
  • ✔ Lifetime Membership
  • ✔ Daily App Updates
Join Membership →