The pure CSS way
The native way is actually quite simple. Design CSS for one color scheme, then override values for the other using the
prefers-color-scheme media feature.
The user’s preference value is read from the operating system or the browser’s own setting. Life is simple, but there’s one glaring omission — letting the user set this preference at a more granular website or page level. For instance, a user might set a preference for dark mode in their browser, but would want to switch to light mode for a text-heavy page.
prefers-color-scheme feature is still used to start with, and clicking the button then applies the alternate class based on the current detected theme.
The CSS is messier, and grows unwieldy as the site’s style expands. As a convenience, it’s also common to save the user’s toggled theme to local storage so that it is automatically loaded on their next visit.
prefers-color-scheme feature, and without any custom classes. It requires looping through every stylesheet’s rules, inspecting the media of each one, and swapping the light and dark color themes out. The code also includes storing the user’s preference in localStorage, so it remembers on page refresh.
The code involved is somewhat complicated and unoptimized and will probably be slow for heavy stylesheets. The CSSStyleSheet and CSSRule APIs aren’t widely used nor are they well documented. However, it works, so it could be considered the best of both worlds: it respects the user’s choice at a granular site level, while still allowing the use of native CSS features.
A further enhancement is to listen to any operating system or browser level preference changes and adjust the applied theme accordingly. This can be done by adding a listener,
window.matchMedia('(prefers-color-scheme: dark)').addListener(...) and reapplying the themes.
Fixing the white flash
Sadly, the hackiness (or its lesser alternative) still isn’t enough. In certain scenarios, when there is a lot of content on the page and the user has saved a dark theme preference for the site, there will briefly appear a blinding white flash before the dark theme activates.
On the fence
Looking at this from a high level, I feel that the work and modifications involved in providing a user toggle takes me a step too far from focusing on the content-first nature of a web page. I’d prefer a more ‘native’ way of achieving the same thing; I did try searching for whether there were any standards, discussions or proposals in place, but couldn’t find any.
For my own purposes I am using this extension, it toggles the browser’s own light and dark mode preference.