{"id":38,"date":"2026-03-14T03:30:30","date_gmt":"2026-03-14T03:30:30","guid":{"rendered":"https:\/\/alexevans.io\/blog\/?p=38"},"modified":"2026-03-14T03:31:19","modified_gmt":"2026-03-14T03:31:19","slug":"dark-mode-implementations","status":"publish","type":"post","link":"https:\/\/alexevans.io\/blog\/dark-mode-implementations\/","title":{"rendered":"Dark mode implementations"},"content":{"rendered":"<h1>Dark Mode Implementations<\/h1>\n<h2>Introduction<\/h2>\n<p>Dark mode looks simple until you&#8217;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\u2014that&#8217;s what most developers assume going in.<\/p>\n<p>This guide isn&#8217;t about whether dark mode looks cool. It does. It&#8217;s about building it correctly, knowing where to invest real effort, and recognizing which corners are safe to cut. Whether you&#8217;re implementing it for the first time or inheriting something that barely works, the same principles apply.<\/p>\n<p>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&#8217;re already behind. The question isn&#8217;t whether to implement it\u2014it&#8217;s how to do it without creating two parallel design systems that drift apart and become a maintenance nightmare.<\/p>\n<p>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&#8217;re a founder or product team with limited bandwidth.<\/p>\n<hr \/>\n<h2>Why This Matters Now<\/h2>\n<p>The shift toward dark mode isn&#8217;t purely aesthetic. There&#8217;s functional reasoning behind it.<\/p>\n<p>On OLED and AMOLED screens, dark pixels consume less power\u2014battery 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&#8217;re competing for trust in a premium market.<\/p>\n<p>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\u2014<code>color-background<\/code> instead of <code>#ffffff<\/code>\u2014implement dark mode in an afternoon. Teams that scattered hardcoded hex values across 80 components spend a week on it.<\/p>\n<p>There&#8217;s a broader signal here. Your product&#8217;s ability to adapt to a user&#8217;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.<\/p>\n<hr \/>\n<h2>Key Considerations<\/h2>\n<h3>Start with a CSS custom properties architecture<\/h3>\n<p>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.<\/p>\n<pre><code class=\"language-css\">:root {\n  --color-background: #ffffff;\n  --color-text-primary: #111111;\n  --color-text-secondary: #555555;\n  --color-surface: #f4f4f5;\n  --color-border: #e4e4e7;\n  --color-accent: #6366f1;\n}\n\n[data-theme=&quot;dark&quot;] {\n  --color-background: #0f0f0f;\n  --color-text-primary: #f1f1f1;\n  --color-text-secondary: #a1a1aa;\n  --color-surface: #1a1a1a;\n  --color-border: #27272a;\n  --color-accent: #818cf8;\n}\n<\/code><\/pre>\n<p>Set <code>data-theme<\/code> on the <code>&lt;html&gt;<\/code> element. Everything else inherits. You never fork your component styles\u2014you reference the variable, and the variable does the work.<\/p>\n<p>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&#8217;t optional. It&#8217;s the entire strategy.<\/p>\n<h3>Respect system preferences by default<\/h3>\n<p>Before any user has set a preference in your product, respect what their OS already knows. The <code>prefers-color-scheme<\/code> media query exists for exactly this.<\/p>\n<pre><code class=\"language-css\">@media (prefers-color-scheme: dark) {\n  :root {\n    --color-background: #0f0f0f;\n    --color-text-primary: #f1f1f1;\n    \/* ... *\/\n  }\n}\n<\/code><\/pre>\n<p>If you&#8217;re offering a manual toggle too, the logic works like this: system preference is the default, user preference stored in <code>localStorage<\/code> overrides it, and the <code>data-theme<\/code> attribute reflects whichever wins. The key is setting that attribute as early as possible\u2014inline in the <code>&lt;head&gt;<\/code>\u2014to prevent a flash of the wrong theme on load.<\/p>\n<pre><code class=\"language-html\">&lt;script&gt;\n  const saved = localStorage.getItem('theme');\n  const preferred = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n  document.documentElement.setAttribute('data-theme', saved || preferred);\n&lt;\/script&gt;\n<\/code><\/pre>\n<p>That single inline script, placed before any CSS loads, eliminates the flash. It&#8217;s one of the most commonly missed steps. Skip it and users see a white burst before dark mode kicks in\u2014jarring, and it feels broken.<\/p>\n<h3>Handle images, icons, and third-party embeds separately<\/h3>\n<p>Custom properties handle your own components well. They don&#8217;t automatically fix:<\/p>\n<ul>\n<li>Images that only work on white backgrounds<\/li>\n<li>SVG icons with hardcoded fill colors<\/li>\n<li>Third-party embeds that render their own UI<\/li>\n<li>Charts, graphs, and canvas elements<\/li>\n<li>Screenshots and documentation images<\/li>\n<\/ul>\n<p>For SVGs you control, use <code>currentColor<\/code> instead of hardcoded fills wherever possible. For images that only work light, CSS can help:<\/p>\n<pre><code class=\"language-css\">@media (prefers-color-scheme: dark) {\n  .logo-image {\n    filter: invert(1) hue-rotate(180deg);\n  }\n}\n<\/code><\/pre>\n<p>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 <code>&lt;picture&gt;<\/code> with a <code>media<\/code> attribute.<\/p>\n<p>Third-party embeds are harder. Many support their own dark mode parameters\u2014check the API or embed docs before writing workarounds. If they don&#8217;t support it, decide whether rebuilding the functionality natively is worth the effort.<\/p>\n<h3>Use semantic color names, not descriptive ones<\/h3>\n<p>This is the decision you&#8217;ll either thank yourself for or regret.<\/p>\n<p>Naming a token <code>--color-gray-100<\/code> describes what it looks like. Naming it <code>--color-surface<\/code> describes what it does. In dark mode, <code>--color-gray-100<\/code> makes no conceptual sense as a dark background. <code>--color-surface<\/code> does.<\/p>\n<p>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&#8217;t.<\/p>\n<p>If you&#8217;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 <code>--color-white<\/code> set to <code>#1a1a1a<\/code>\u2014and a team that&#8217;s stopped trusting the design system entirely.<\/p>\n<h3>Test transitions and animation<\/h3>\n<p>Color transitions during theme switches can feel polished or glitchy. A simple transition on background and color goes a long way:<\/p>\n<pre><code class=\"language-css\">:root {\n  transition: background-color 0.2s ease, color 0.2s ease;\n}\n<\/code><\/pre>\n<p>Don&#8217;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.<\/p>\n<p>Also test what happens when the system theme changes while your app is open. Most apps ignore this entirely. It&#8217;s worth handling via a <code>matchMedia<\/code> listener so users with automatic light\/dark switching see it reflected without a page reload.<\/p>\n<h3>What to skip if you have limited bandwidth<\/h3>\n<p>Small team, shipping fast? Here&#8217;s the honest prioritization.<\/p>\n<p><strong>Do first:<\/strong> Custom properties architecture, system preference detection, inline script for flash prevention.<\/p>\n<p><strong>Do when you have the cycles:<\/strong> Manual user toggle, preference persistence, theme-aware images.<\/p>\n<p><strong>Skip for now:<\/strong> 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.<\/p>\n<p>Dark mode at 80 percent beats dark mode not shipped\u2014as 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.<\/p>\n<hr \/>\n<h2>Next Steps<\/h2>\n<p>Starting from a codebase with no theming infrastructure? The path is:<\/p>\n<ol>\n<li>Audit your current color usage. Find the hardcoded values.<\/li>\n<li>Build a semantic token map\u20148 to 12 tokens covers most interfaces.<\/li>\n<li>Replace hardcoded values with <code>var(--token-name)<\/code> across components.<\/li>\n<li>Add the <code>[data-theme=\"dark\"]<\/code> overrides.<\/li>\n<li>Add the inline <code>&lt;head&gt;<\/code> script for flash prevention.<\/li>\n<li>Test on OLED mobile devices and in low-light conditions.<\/li>\n<li>Handle edge cases: SVGs, images, third-party embeds.<\/li>\n<\/ol>\n<p>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\u2014applied by one team without updating the system\u2014create a two-track problem. The whole point is a single layer that everything inherits from.<\/p>\n<p>If you&#8217;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.<\/p>\n<p>From an <a href=\"https:\/\/alexevans.io\/\">AI-first web design perspective<\/a>, 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&#8217;s a design system that&#8217;s been thought through rather than bolted together.<\/p>\n<p>If your product is due for a rebuild or a serious audit\u2014and dark mode is one of several things not working cleanly\u2014that&#8217;s worth a focused conversation rather than piecemeal fixes.<\/p>\n<hr \/>\n<h2>Conclusion and CTA<\/h2>\n<p>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.<\/p>\n<p>The core approach isn&#8217;t complicated: CSS custom properties with semantic naming, system preference detection, an inline script that prevents flash, and disciplined handling of assets you don&#8217;t fully control. The details\u2014transitions, third-party embeds, image variants\u2014are refinements you layer in after the foundation is solid.<\/p>\n<p>What this isn&#8217;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&#8217;ll be revisiting it every time a new component joins the system.<\/p>\n<p>The teams I&#8217;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.<\/p>\n<hr \/>\n<p>Your site should earn its keep. <a href=\"https:\/\/alexevans.io\/#contact\">Request an AI and website teardown<\/a>\u2014no pitch, just a clear view of what&#8217;s working and what isn&#8217;t.<\/p>\n<hr \/>\n<p><em>Meta Title:<\/em> Dark Mode Implementations: A Practical Guide for Developers<br \/>\n<em>Meta Description:<\/em> Learn how to build dark mode correctly using CSS custom properties, system preference detection, and semantic token architecture. A practical implementation guide.<br \/>\n<em>Twitter:<\/em> Most dark mode problems aren&#8217;t toggle problems. They&#8217;re token problems. Here&#8217;s how to build it so it actually holds up.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Dark mode implementations: practical guide for founders and teams. What to do first and what to skip.<\/p>\n","protected":false},"author":1,"featured_media":37,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"ae_seo_meta_title":"Dark mode implementations","ae_seo_meta_description":"Dark mode implementations: practical guide for founders and teams. What to do first and what to skip. From Alex Evans \u2014 AI Architect and Software Engineer.","ae_seo_keywords":"Dark mode implementations examples, Dark mode color palette generator, Dark mode implementations reddit, Dark mode implementations github, Dark mode implementations, Websites with dark mode toggle","ae_seo_primary_keyword":"Dark mode implementations","ae_seo_twitter_card_title":"","ae_seo_twitter_card_description":"","footnotes":""},"categories":[18,4,3],"tags":[],"class_list":["post-38","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dark-mode-design","category-ui-ux-design","category-webdesign"],"_links":{"self":[{"href":"https:\/\/alexevans.io\/blog\/wp-json\/wp\/v2\/posts\/38","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/alexevans.io\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/alexevans.io\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/alexevans.io\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/alexevans.io\/blog\/wp-json\/wp\/v2\/comments?post=38"}],"version-history":[{"count":1,"href":"https:\/\/alexevans.io\/blog\/wp-json\/wp\/v2\/posts\/38\/revisions"}],"predecessor-version":[{"id":39,"href":"https:\/\/alexevans.io\/blog\/wp-json\/wp\/v2\/posts\/38\/revisions\/39"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/alexevans.io\/blog\/wp-json\/wp\/v2\/media\/37"}],"wp:attachment":[{"href":"https:\/\/alexevans.io\/blog\/wp-json\/wp\/v2\/media?parent=38"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/alexevans.io\/blog\/wp-json\/wp\/v2\/categories?post=38"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/alexevans.io\/blog\/wp-json\/wp\/v2\/tags?post=38"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}