In short
Implement dark mode by defining semantic CSS variables once and providing two value sets — one per theme — scoped under a data attribute or class, toggled via user preference or system setting, so every component automatically adopts the correct theme without any component-level dark-mode-specific code.
The implementation goal
The correct implementation target: components never contain dark-mode-specific code at all — they reference semantic variables (color-surface-primary), and dark mode is purely a different set of values for those same variable names, applied at a higher scope. If a component needs its own conditional logic for dark mode, that's a sign the token structure isn't doing its job.
Scoping theme values
Define light-mode values on :root and dark-mode overrides scoped under a data attribute or class:
``css :root { --color-surface-primary: #ffffff; --color-text-primary: #151715; } [data-theme="dark"] { --color-surface-primary: #0b0b0f; --color-text-primary: #f5f5f7; } ``
Every component references var(--color-surface-primary) without any awareness of which theme is active — the cascade handles it entirely.
Respecting system preference
Use the prefers-color-scheme media query to default to the user's system-level preference automatically:
``css @media (prefers-color-scheme: dark) { :root { / dark values, or apply [data-theme="dark"] variables directly here / } } ``
Combine this with a manual override (a toggle that sets data-theme explicitly) so users who want to override their system default for just this site can do so, with the manual choice taking precedence over the system default.
Persisting a manual choice
If you offer a manual dark-mode toggle, persist the user's choice (typically in localStorage) and apply it on page load before the page paints, to avoid a visible flash of the wrong theme — this requires a small inline script in the document head that runs before the main stylesheet, rather than waiting for a full JavaScript bundle to load and apply the theme after the page has already rendered incorrectly.
What needs specific attention beyond color variables
Shadows often need a different treatment in dark mode — a shadow that reads clearly on a light background can become invisible or look wrong on a dark one, and is often better replaced with a subtle border or lighter overlay in the dark variant. Images and illustrations designed for a light background may also need a dark-mode-specific treatment; see how to design dark mode without doubling your work for the design-side considerations.
Avoiding a duplicated stylesheet
The anti-pattern to avoid is maintaining two nearly-identical, separate stylesheets for light and dark mode — this duplicates maintenance effort and virtually guarantees the two drift out of sync over time. A single stylesheet with theme-scoped variable overrides, as described above, avoids this entirely by keeping one set of component styles that simply read different variable values per theme.
Testing both themes as part of normal development
Once implemented, treat both themes as equally real — review new components and features in both light and dark mode as a standard part of the review process, not as an occasional separate check, since drift accumulates quickly if only one theme is actively verified during regular development.
Related on Troiana: How to Write CSS That Doesn't Fight You.
Reference: the authoritative guidance lives at MDN Web Docs.
Common questions
Should components contain their own dark-mode-specific styling code?
No — the goal is components that only reference semantic variables, with dark mode purely a different value mapping for those variables at a higher scope; component-level conditional styling suggests the token structure needs rework.
How do I avoid a flash of the wrong theme on page load?
Persist the user's manual theme choice and apply it via a small inline script in the document head that runs before the page paints, rather than waiting for the main JavaScript bundle to load and apply it afterward.
Does dark mode require a completely separate stylesheet?
No, and it shouldn't — a single stylesheet with theme-scoped CSS variable overrides avoids the duplication and drift risk of maintaining two nearly-identical stylesheets.