Blog

How to Build Dark Mode with Design Tokens in React

Build maintainable React dark mode with semantic design tokens, accessible contrast, system preferences, and stable theme initialization.

ReactDark ModeDesign TokensTheming
Dark-themed code editor displayed on a monitor.
Photo by Harshit Katiyar on Unsplash

Dark mode looks simple in a mockup: swap the background, lighten the text, and adjust a few surfaces. In a real React application, every component state, border, overlay, chart, illustration, and third-party widget has to participate in the theme.

The maintainable solution is not a parallel set of ad hoc dark colors. It is a semantic token system that describes what each color does.

Separate purpose from raw color

A raw palette might include neutral values and brand hues. Components should consume semantic roles instead:

  • Page background.
  • Raised surface.
  • Primary text.
  • Muted text.
  • Subtle border.
  • Primary action.
  • Danger surface and text.
  • Focus indicator.

The light and dark themes assign different palette values to those roles. A card asks for the raised-surface token rather than checking whether the active theme is dark.

This separation makes components easier to reuse and themes easier to change. It also reduces conditional styling scattered across the codebase. For the broader architecture, see the practical guide to design tokens in React.

Design dark mode instead of inverting light mode

Simple inversion produces harsh contrast, strange brand colors, and misleading status treatments. Dark surfaces usually benefit from a controlled range of near-black tones rather than absolute black everywhere.

Elevation may also work differently. A shadow that separates surfaces in a light theme can disappear on a dark background. Subtle borders, lighter raised surfaces, or a combination of cues may communicate hierarchy more effectively.

Review images and illustrations too. Assets with baked-in white backgrounds can look broken, while transparent logos may need a theme-specific variant.

Check every interactive state

Contrast checks must include more than default text. Test hover, focus, selected, disabled, placeholder, error, and loading states.

A dark button can have sufficient contrast at rest and become unreadable on hover. A focus ring that is clear on the page background may disappear against a selected navigation item. Disabled controls still need to be recognizable even if they are intentionally subdued.

Do not use opacity as the only strategy for state styling. Layered opacity can produce unpredictable results as backgrounds change.

Respect user preference without removing choice

The operating system’s color-scheme preference is a useful initial default. After the user chooses a theme explicitly, preserve that choice.

A common theme control offers light, dark, and system options. “System” continues to follow the operating system, while the explicit choices remain stable until changed.

The control needs a clear accessible name and current state. It should work with a keyboard and avoid relying on sun and moon icons alone.

Prevent flashes and hydration mismatches

The correct theme should be applied before the first visible render whenever possible. If the application waits for client-side React code to read a saved setting, users may see the light theme flash before dark mode appears.

Theme initialization must coordinate server output, a stored preference, and the system setting. The approach depends on the application architecture, but the result should be a stable initial document. Our guide to theming Next.js without flashes or hydration issues explores this problem in detail.

Give charts and data visualization their own tokens

Dashboard charts need more than background and text colors. Series colors must remain distinguishable against the dark plotting area and from one another. Grid lines, tooltips, selection states, and threshold indicators all need deliberate values.

Avoid depending on hue alone. Use labels, patterns, direct annotations, or shape differences where they improve comprehension.

Test dark mode as a complete product

Create a small theme test page containing typography, buttons, fields, menus, dialogs, alerts, tables, and charts in every state. Then test actual workflows at different brightness levels and in high-contrast settings.

Finally, add visual regression coverage for representative screens. Token changes can affect the entire product, so broad feedback is valuable.

Dark mode is successful when it feels like the same product under different viewing conditions. Semantic tokens create that continuity while keeping the implementation understandable as the interface grows.