← All Posts / CSS & StylingJune 2026 / 6 min read / 310 words
Dark Mode Done Right: Theme Switching with React Context and CSS Variables

Dark Mode Done Right: Theme Switching with React Context and CSS Variables

Dark mode sounds like a CSS problem until the moment you need to persist the choice, avoid a flash on load, and let any component read the current theme. That part is a small, well-contained state problem, and React Context handles it without extra dependencies.

TL;DR / Written for skimmers

A single ThemeProvider near the root of the app is enough. It owns the current theme, exposes a toggle function, and applies the corresponding class to the html element as a side effect. Reading the saved theme before the first paint, rather than after the component mounts, is what prevents the brief flash of the default theme. A tiny inline script in the document head, or a server-aware read, both solve this the same way: apply the class before React even renders. Once the class is on html, the actual color values live entirely in CSS, defined once for light and once for dark. The Context is only responsible for which class is active, never for the colors themselves. The most useful test for a theme toggle is manual and simple: toggle it, reload the page, and confirm the choice survived. Most regressions in this area come from a missed localStorage read on load, not from the toggle logic itself.

Where theme state should actually live

A single ThemeProvider near the root of the app is enough. It owns the current theme, exposes a toggle function, and applies the corresponding class to the html element as a side effect.

Keeping the state in one provider, instead of letting every component check localStorage itself, avoids the inconsistent states that show up when multiple places try to own the same decision.

Persisting the choice without a flicker on reload

Reading the saved theme before the first paint, rather than after the component mounts, is what prevents the brief flash of the default theme. A tiny inline script in the document head, or a server-aware read, both solve this the same way: apply the class before React even renders.

Writing to localStorage on every toggle keeps the preference around for the next visit without needing an account or a backend involved.

Letting CSS variables do the heavy lifting

Once the class is on html, the actual color values live entirely in CSS, defined once for light and once for dark. The Context is only responsible for which class is active, never for the colors themselves.

This split keeps the JavaScript side small and testable, since toggling a class is much easier to reason about than swapping a large theme object through props.

Testing the toggle like a user would

The most useful test for a theme toggle is manual and simple: toggle it, reload the page, and confirm the choice survived. Most regressions in this area come from a missed localStorage read on load, not from the toggle logic itself.

Checking that interactive elements stay legible in both themes, especially focus states and disabled buttons, catches the contrast issues that automated tests rarely flag.

← All Posts
ReactCSSDark ModeNext.js