How to implement dark mode with CSS variables step by step

  • CSS variables allow you to centralize colors and styles, making it easier to create light and dark themes without duplicating rules.
  • The media query prefers-color-scheme automatically adapts dark mode to the user's operating system preference.
  • A manual selector with classes or data-theme, combined with JavaScript and localStorage, offers complete and persistent control of the theme.
  • Paying attention to contrast, typography, and visual feedback in dark mode improves accessibility and user experience on all types of devices.

Implement dark mode using CSS variables

If you spend a lot of time in front of a screen, you've probably appreciated being able to activate dark mode on an app or website more than once. It's not just about aesthetics: a good dark theme can reduce eye strain , help with concentration, and, on some devices, even save battery life.

The good news is that setting up a dark mode on your site is no big deal. With a combination of CSS variables, media queries, and a little optional JavaScript, you can create flexible, accessible, and easy-to-maintain light and dark themes without rewriting your entire stylesheet.

What exactly is dark mode and why use it?

When we talk about dark mode, we 're referring to a color scheme where backgrounds become dark and the main content is displayed in light tones . It's the opposite of the classic design of dark text on a white background, which has been the norm on the web for years.

Modern operating systems (Windows, macOS, iOS, Android, etc.) allow you to configure a global preference for light or dark themes . Many users activate dark mode in low-light environments, either for comfort or simply for aesthetic preference. If your website respects this choice, you're providing a significant advantage in user experience and accessibility.

Furthermore, on OLED and AMOLED screens, black or almost black pixels consume less energy, so a well-designed dark mode can help save battery , especially on mobile phones.

Basic principles of dark mode design

It's not just about inverting colors. For a dark theme to work, several design and accessibility details need to be considered, because poorly implemented contrast can be just as annoying as an overly bright interface.

Contrast and legibility

The first critical point is the contrast between background and text. You need the content to be easy to read on dark backgrounds without causing glare . The WCAG guidelines recommend a minimum ratio of 4.5:1 for normal text, although in dark mode it's very common to aim for values ​​around 7:1 to improve readability.

A typical example would be using an almost black background and light gray text, not pure white. For instance, something like background-color: #121212 and color: #E0E0E0 usually produces a smooth and legible result, avoiding the harshness of pure white on total black.

Choice of palette and accent colors

In dark themes, it's best to avoid highly saturated and garish colors , because they tend to stand out too much against a dark background and quickly strain the eyes. It's better to opt for softer, slightly desaturated tones for buttons, links, and highlighted elements.

It's also advisable to avoid absolute black (#000000) and pure white (#FFFFFF). A slightly darker black and a somewhat muted white create a more pleasing contrast. For example, instead of using pure white for an accent, you could choose something like #BB86FC or a soft gold that stands out without being garish.

Accessibility and user preferences

Designing a dark mode involves considering users with varying visual abilities. It's not enough for it to "look nice"; it also needs to be usable for people with low vision or sensitivity to brightness . This includes reviewing contrast, focus states, error states, and interactive elements.

In addition to color, it's important to respect other preferences such as reduced motion . If the user has indicated a preference for fewer animations, your transitions between themes should be adapted using, for example, a media query ` prefers-reduced-motion` to reduce or eliminate animations in those cases.

Why CSS variables are the key to dark mode

CSS variables (custom properties) are ideal for managing a theme system because they allow Centralize all theme-dependent colors and styles in one placeInstead of repeating values ​​throughout the stylesheet, you define a few variables and then reuse them with the function. var().

It is usual to declare these variables in the pseudo-class :root, which points to the element <html>For example, you can define base colors for the light theme thus:

:root {
  --color-fondo: #ffffff;
  --color-texto: #333333;
  --color-acento: #007BFF;
}

From that point on, the rest of the stylesheet uses these custom properties instead of fixed values :

body {
  background-color: var(--color-fondo);
  color: var(--color-texto);
}

a,
button {
  color: var(--color-acento);
}

If you decide to change the palette or add a dark theme, you will only need to reassign the values ​​of those variables in another context (classes, attributes, media queries…) instead of reviewing each rule one by one.

Implement dark mode using media queries (prefers-color-scheme)

The most user-friendly approach is to let the system dictate the terms. The media query prefers-color-scheme it allows you Automatically adapt the theme to the device's global preference..

There are two commonly used approaches: defining separate versions for light and dark, or treating light mode as the default and overwriting it only when the system requests dark.

Define variables by mode with media queries

A very straightforward pattern is to declare base variables and then change them within specific media queries:

/* Tema claro por defecto */
:root {
  --body-bg: #FFFFFF;
  --body-color: #000000;
}

/* Tema oscuro cuando el usuario lo prefiere */
@media (prefers-color-scheme: dark) {
  :root {
    --body-bg: #000000;
    --body-color: #FFFFFF;
  }
}

In this example, the body will always use background: var(–body-bg) and color: var(–body-color)The only thing that changes is the value of the variables according to the system's preference. If more values ​​were added to the system in the future... prefers-color-schemeYou would still have a properly defined default clear mode.

You can also be even more explicit and specify both modes:

@media (prefers-color-scheme: light) {
  :root {
    --body-bg: #FFFFFF;
    --body-color: #000000;
  }
}

@media (prefers-color-scheme: dark) {
  :root {
    --body-bg: #000000;
    --body-color: #FFFFFF;
  }
}

This allows your site to automatically adapt to the light or dark mode chosen in the operating system , without the user having to touch any settings within the website.

Dark mode without JavaScript using :has() and a checkbox

If you want to offer a theme switch within the page but don't feel like using JavaScript, you can use a relational selector. :has()This selector allows you to apply styles to a parent element when It contains a child that meets a certain condition., in this case a checked checkbox.

The idea is to first define the light colors as a base:

:root {
  --bg-color: #ffffff;
  --text-color: #222222;
}

body {
  background: var(--bg-color);
  color: var(--text-color);
  transition: background 0.3s, color 0.3s;
}

And then take advantage :has() To change the variables when the dark mode checkbox is enabled:

body:has(#darkmode-toggle:checked) {
  --bg-color: #1e1e1e;
  --text-color: #f5f5f5;
}

In HTML you only need one checkbox to act as a switch :

<input type="checkbox" id="darkmode-toggle" />
<label for="darkmode-toggle">Modo oscuro</label>

When the user checks the box, the selector `body:has(#darkmode-toggle:checked)` comes into play and the variables change to dark mode values. The major drawback of this approach is that the preference is not retained across pages or visits because no additional storage or logic is used.

Theme switcher with classes, JavaScript, and localStorage

If you're looking for a more complete system, the usual approach is to add or remove a class (or a data attribute) in the root element and manage the change with JavaScript, also saving the user's choice.

Define variables according to the topic class

First, you define the default light colors in :root and then an obscure variant when the root element has a concrete class, such as tema-oscuro:

:root {
  --color-fondo: #ffffff;
  --color-texto: #000000;
  --color-principal: #007bff;
}

:root.tema-oscuro {
  --color-fondo: #333333;
  --color-texto: #ffffff;
  --color-principal: #BB86FC;
}

The other styles continue to use var(-background-color), var(-text-color) and other variablesso changing the subject is equivalent to switching classes tema-oscuro en <html> o <body>.

Create the switch in HTML and bring it to life with JavaScript.

In the markup, you can use a simple checkbox, a button, or a more elaborate toggle. A simple example would be:

<input type="checkbox" id="interruptor-tema" />
<label for="interruptor-tema">Tema oscuro</label>

With JavaScript, you listen for the toggle change and add or remove the class on the root element, as well as save the preference in localStorage so it's not lost when navigating or reloading:

// Al cargar la página, aplicamos el tema guardado
if (localStorage.getItem('tema') === 'oscuro') {
  document.documentElement.classList.add('tema-oscuro');
  document.getElementById('interruptor-tema').checked = true;
}

// Actualizamos cuando el usuario cambia el toggle
document.getElementById('interruptor-tema').addEventListener('change', function () {
  if (this.checked) {
    document.documentElement.classList.add('tema-oscuro');
    localStorage.setItem('tema', 'oscuro');
  } else {
    document.documentElement.classList.remove('tema-oscuro');
    localStorage.setItem('tema', 'claro');
  }
});

This approach gives you a persistent, user-controlled dark mode , while still taking advantage of CSS variables to keep the code clean.

Use data attributes or data-theme as a selector

Instead of a class, you can also use an attribute like data-themeThis is especially convenient if you work with frameworks or utilities like Tailwind CSS, which allow you to configure the variant. dark so that it activates when there is a selector of the type instead of the media query.

In native CSS the pattern would be very similar:

:root {
  --color-fondo: #FFFFFF;
  --color-texto: #333333;
  --color-acento: #007BFF;
}

 {
  --color-fondo: #121212;
  --color-texto: #E0E0E0;
  --color-acento: #BB86FC;
}

Applying the attribute to the root element:

<html lang="es" data-theme="dark">

The entire site will switch to using dark values. If you change the attribute to light Or you remove it, and you'll be back to the clear topic. The JavaScript for switching between one and the other doesn't change much compared to the case with classes: you simply call setAttribute('data-theme', 'dark') o light depending on the user's choice.

Combine system theme and manual selector

A very common scenario is wanting to support three states: light theme, dark theme, and "use system theme"To do this you can combine prefers-color-scheme with classes or attributes, and use window.matchMedia() to detect the system mode when the user chooses to follow that option.

The general idea is:

  • If the user explicitly selects light or dark, you save that preference and apply it with a class or attribute (for example, data-theme="dark").
  • If you choose "system", you don't force any specific theme and let the media queries take over. prefers-color-scheme those that decide the default values.
  • Optionally, you listen for changes in matchMedia('(prefers-color-scheme: dark)') to react if the user changes the system theme on the fly.

This pattern fits very well with tools like Tailwind, where you can decide if the variant dark it's based on prefers-color-scheme or in a class/attribute, and even synchronize it with a preference stored on the client or server.

dark mode windows 11
Related article:
How to activate dark mode in Windows 11

Beyond color: typography and interface elements

Activating dark mode shouldn't be limited to just changing four colors. There are other details worth adjusting to make the experience truly comfortable and consistent.

First, it's advisable to slightly increase the font size and line spacing on dark themes, because text on a dark background tends to appear denser. A small increase in base size or line height can make a big difference.

It's also a good idea to review buttons, links, cards, and other UI components. In dark mode, you can use soft shadows, subtle borders, or very slight gradients to create a sense of depth without resorting to flat backgrounds that make everything look like it's stuck on the same level.

Don't forget visual feedback: hover, focus, and active states must remain clear. In a dark theme, a color change or a slight increase in the brightness of the accent color usually works better than an overly aggressive shadow.

Testing, accessibility, and validation tools

Once you've set up your theme system, it's time to really test it. It's not enough to just see it in your main browser; it's important to verify that dark mode works correctly on different devices, browsers, and configurations.

Some useful tools for this phase are the DevTools for Chrome (try how to force dark mode in Google Chrome) , Firefox, or Safari, which include options to simulate prefers-color-scheme without changing your system theme. You can also use Lighthouse to check accessibility and contrast, or cross-testing services like CrossBrowserTesting to verify that styles aren't broken in less common browsers.

In addition to technical testing, it's worthwhile to gather real user feedback . Short surveys, in-site forms, or user testing sessions can reveal readability issues, insufficient contrast, or elements that go unnoticed in dark mode.

Implementing a modern dark mode with CSS variables allows you to respect system preferences, offer a flexible theme selector, keep code clean, and ensure accessibility—something increasingly valued by users and search engines. With a solid foundation of variables, well-designed media queries, and, when necessary, a touch of JavaScript for persistence, your site can seamlessly transition between light and dark themes without becoming a chaotic mess of unmaintainable styles.


Add as preferred source in Google