March 14, 2026 — Dark Mode Design, UI/UX Design, Web Design

Dark mode implementations

Dark Mode Implementations

Introduction

Dark mode looks simple until you’re three hours deep in a z-index issue, a mismatched SVG, and a modal rendering white-on-white. A toggle, a color switch, done—that’s what most developers assume going in.

This guide isn’t about whether dark mode looks cool. It does. It’s about building it correctly, knowing where to invest real effort, and recognizing which corners are safe to cut. Whether you’re implementing it for the first time or inheriting something that barely works, the same principles apply.

Dark mode is a baseline user expectation now, not a nice-to-have. Over 80 percent of smartphone users enable it when available. If your product ignores system preferences, you’re already behind. The question isn’t whether to implement it—it’s how to do it without creating two parallel design systems that drift apart and become a maintenance nightmare.

This post covers the practical path: the CSS approach that scales, how to handle user preferences correctly, where theming breaks down, and what to prioritize when you’re a founder or product team with limited bandwidth.


Why This Matters Now

The shift toward dark mode isn’t purely aesthetic. There’s functional reasoning behind it.

On OLED and AMOLED screens, dark pixels consume less power—battery life improves meaningfully on mobile. For users spending six or more hours a day in a product, eye strain reduction is real, not marketing copy. And from a product retention angle, ignoring system-level preferences signals that your product is slightly behind. That matters when you’re competing for trust in a premium market.

From a development perspective, dark mode has become a test of how well your design system is actually built. Teams that chose semantic color naming over literal values—color-background instead of #ffffff—implement dark mode in an afternoon. Teams that scattered hardcoded hex values across 80 components spend a week on it.

There’s a broader signal here. Your product’s ability to adapt to a user’s environment is a proxy for how thoughtfully it was built overall. A site that flashes white on load, or renders links in a color that disappears against a dark background, loses a layer of trust before a word is read. Founders pitching premium services or enterprise clients feel that, even if no one names it.


Key Considerations

Start with a CSS custom properties architecture

The foundation of any robust dark mode implementation is CSS custom properties scoped to a theme attribute or class. Not inline styles. Not JavaScript-heavy theme objects passed through context. CSS at the root.

:root {
  --color-background: #ffffff;
  --color-text-primary: #111111;
  --color-text-secondary: #555555;
  --color-surface: #f4f4f5;
  --color-border: #e4e4e7;
  --color-accent: #6366f1;
}

[data-theme="dark"] {
  --color-background: #0f0f0f;
  --color-text-primary: #f1f1f1;
  --color-text-secondary: #a1a1aa;
  --color-surface: #1a1a1a;
  --color-border: #27272a;
  --color-accent: #818cf8;
}

Set data-theme on the <html> element. Everything else inherits. You never fork your component styles—you reference the variable, and the variable does the work.

Most teams who struggle with dark mode are maintaining two separate visual states instead of one token layer. One source of truth for colors isn’t optional. It’s the entire strategy.

Respect system preferences by default

Before any user has set a preference in your product, respect what their OS already knows. The prefers-color-scheme media query exists for exactly this.

@media (prefers-color-scheme: dark) {
  :root {
    --color-background: #0f0f0f;
    --color-text-primary: #f1f1f1;
    /* ... */
  }
}

If you’re offering a manual toggle too, the logic works like this: system preference is the default, user preference stored in localStorage overrides it, and the data-theme attribute reflects whichever wins. The key is setting that attribute as early as possible—inline in the <head>—to prevent a flash of the wrong theme on load.

<script>
  const saved = localStorage.getItem('theme');
  const preferred = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  document.documentElement.setAttribute('data-theme', saved || preferred);
</script>

That single inline script, placed before any CSS loads, eliminates the flash. It’s one of the most commonly missed steps. Skip it and users see a white burst before dark mode kicks in—jarring, and it feels broken.

Handle images, icons, and third-party embeds separately

Custom properties handle your own components well. They don’t automatically fix:

  • Images that only work on white backgrounds
  • SVG icons with hardcoded fill colors
  • Third-party embeds that render their own UI
  • Charts, graphs, and canvas elements
  • Screenshots and documentation images

For SVGs you control, use currentColor instead of hardcoded fills wherever possible. For images that only work light, CSS can help:

@media (prefers-color-scheme: dark) {
  .logo-image {
    filter: invert(1) hue-rotate(180deg);
  }
}

Use this selectively. It works well for monochrome logos and icons. It breaks on photography and anything with complex color. For full control, maintain two image variants where it matters and swap them via <picture> with a media attribute.

Third-party embeds are harder. Many support their own dark mode parameters—check the API or embed docs before writing workarounds. If they don’t support it, decide whether rebuilding the functionality natively is worth the effort.

Use semantic color names, not descriptive ones

This is the decision you’ll either thank yourself for or regret.

Naming a token --color-gray-100 describes what it looks like. Naming it --color-surface describes what it does. In dark mode, --color-gray-100 makes no conceptual sense as a dark background. --color-surface does.

Build your token system around roles, not values: surface, background, text-primary, text-secondary, border, accent, accent-hover, error, success. These translate across both themes. Gray-100 through gray-900 don’t.

If you’re inheriting a codebase with descriptive names, do the refactor before implementing dark mode. Layering dark mode on top of descriptively named tokens is how you end up with a token called --color-white set to #1a1a1a—and a team that’s stopped trusting the design system entirely.

Test transitions and animation

Color transitions during theme switches can feel polished or glitchy. A simple transition on background and color goes a long way:

:root {
  transition: background-color 0.2s ease, color 0.2s ease;
}

Don’t transition everything. Borders, shadows, and accent colors all shifting simultaneously produces a muddied animation where nothing feels intentional. Pick the most prominent properties and let the rest snap.

Also test what happens when the system theme changes while your app is open. Most apps ignore this entirely. It’s worth handling via a matchMedia listener so users with automatic light/dark switching see it reflected without a page reload.

What to skip if you have limited bandwidth

Small team, shipping fast? Here’s the honest prioritization.

Do first: Custom properties architecture, system preference detection, inline script for flash prevention.

Do when you have the cycles: Manual user toggle, preference persistence, theme-aware images.

Skip for now: Transitioning every UI element, building a full Storybook dark mode preview environment, supporting older browsers without CSS custom properties. They represent less than half a percent of your traffic and the workarounds cost days.

Dark mode at 80 percent beats dark mode not shipped—as long as the core experience holds. The parts users actually notice are the text, the background, and whether the primary actions are readable. Get those right first.


Next Steps

Starting from a codebase with no theming infrastructure? The path is:

  1. Audit your current color usage. Find the hardcoded values.
  2. Build a semantic token map—8 to 12 tokens covers most interfaces.
  3. Replace hardcoded values with var(--token-name) across components.
  4. Add the [data-theme="dark"] overrides.
  5. Add the inline <head> script for flash prevention.
  6. Test on OLED mobile devices and in low-light conditions.
  7. Handle edge cases: SVGs, images, third-party embeds.

For teams running on a design system or component library, step one is aligning with whoever owns the tokens. Dark mode implementations that go rogue—applied by one team without updating the system—create a two-track problem. The whole point is a single layer that everything inherits from.

If you’re building a new product or redesigning from scratch, implement the token architecture on day one. It costs nothing at that stage and saves a week of refactoring later.

From an AI-first web design perspective, accessibility and adaptability are the same category. A site that handles dark mode well handles high-contrast mode well. It handles future theme variants well. It’s a design system that’s been thought through rather than bolted together.

If your product is due for a rebuild or a serious audit—and dark mode is one of several things not working cleanly—that’s worth a focused conversation rather than piecemeal fixes.


Conclusion and CTA

Dark mode implementations are a proxy for code quality. Not always, but often. Teams with clean design systems ship it in hours. Teams without one ship it in days and come out the other side with tech debt shaped like toggle buttons.

The core approach isn’t complicated: CSS custom properties with semantic naming, system preference detection, an inline script that prevents flash, and disciplined handling of assets you don’t fully control. The details—transitions, third-party embeds, image variants—are refinements you layer in after the foundation is solid.

What this isn’t is a visual styling exercise you run in isolation from the rest of your stack. It touches your token architecture, your component structure, your asset pipeline, and how you store user preferences. Do it with that scope in mind and it holds up. Treat it as a one-afternoon hack and you’ll be revisiting it every time a new component joins the system.

The teams I’ve worked with who skipped the token architecture step are the same ones who ask me to help them clean it up six months later. The ones who invested a day in the foundation stopped thinking about it.


Your site should earn its keep. Request an AI and website teardown—no pitch, just a clear view of what’s working and what isn’t.


Meta Title: Dark Mode Implementations: A Practical Guide for Developers
Meta Description: Learn how to build dark mode correctly using CSS custom properties, system preference detection, and semantic token architecture. A practical implementation guide.
Twitter: Most dark mode problems aren’t toggle problems. They’re token problems. Here’s how to build it so it actually holds up.


Let's work together