Skip to content
HomeWorkProcessAboutBlogContact[email protected]
← Back to blog

Dark Mode Design: A Complete Implementation Guide

Dark Mode Design: A Complete Implementation Guide. dark mode design Insights from The Proper Motion Company.

Dark mode is no longer a novelty feature. As of 2024, over 80% of smartphone users report using dark mode at least some of the time, according to survey data from Android Authority. Both iOS and Android default to dark mode system settings for a growing percentage of new device activations. For web applications and native apps alike, supporting dark mode is a baseline expectation, not a differentiator.

But implementing dark mode well is surprisingly difficult. A naive approach of inverting colors produces an interface that is technically dark but aesthetically broken, functionally compromised, and sometimes genuinely hard to read. This guide covers how to implement dark mode correctly, from color system design through to the CSS and code that makes it work.

Designing a Dark Mode Color System

The most common mistake in dark mode design is starting from your light mode palette and darkening everything. Dark mode needs its own intentional color system designed from scratch, informed by the same brand identity but not derived mechanically from the light palette.

Background layers. Use a layered background approach with 3 to 4 elevation levels. The lowest layer (page background) should be the darkest, typically in the range of #0D0D0D to #1A1A1A. Avoid pure black (#000000) because it creates excessive contrast against text, causing eye strain during extended reading sessions. Each elevation step lightens the background by 4% to 8%. A card sitting on the page might use #1E1E1E. A modal on top of the card might use #252525. A tooltip might use #2C2C2C. Material Design uses exactly this approach, and it works because it preserves the same visual hierarchy cues that shadows provide in light mode.

Text hierarchy. Use opacity-based text colors against your dark backgrounds rather than specific gray values. Primary text at 87% white opacity (#FFFFFFDE), secondary text at 60% (#FFFFFF99), and disabled text at 38% (#FFFFFF61). This approach automatically adapts to any background shade while maintaining consistent contrast ratios. Verify that your primary text meets WCAG AA contrast requirements (minimum 4.5:1 ratio for body text) against every background layer.

Accent and brand colors. Your brand’s primary color probably needs adjustment for dark mode. Saturated colors that look vibrant on white backgrounds often feel harsh and glaring on dark backgrounds. Desaturate your accent colors by 10% to 20% and increase their lightness by 10% to 15%. For example, if your light-mode primary is hsl(220, 90%, 50%), your dark-mode variant might be hsl(220, 70%, 65%). Test both against your dark backgrounds for readability and visual comfort.

Semantic colors. Error reds, success greens, and warning yellows all need dark-mode variants. The standard approach is to use lighter, less saturated versions: shift error red from #DC2626 to #F87171, success green from #16A34A to #4ADE80, warning yellow from #CA8A04 to #FACC15. These lighter variants maintain readability and emotional association without blinding users on dark backgrounds.

Related: White Space in Application Design: Why Less Is More

CSS Implementation with Custom Properties

Modern CSS makes dark mode implementation clean and maintainable. The key is building a design token system that switches at one level while everything else references tokens.

Define your token layers. Create three layers of CSS custom properties. The first layer is primitive values: raw color definitions with no semantic meaning.

:root {
  --gray-50: #fafafa;
  --gray-900: #0f0f0f;
  --blue-500: #3b82f6;
  --blue-300: #93c5fd;
}

The second layer is semantic tokens that reference primitives and carry meaning:

:root {
  --color-bg-primary: var(--gray-50);
  --color-bg-elevated: #ffffff;
  --color-text-primary: var(--gray-900);
  --color-accent: var(--blue-500);
}

[data-theme="dark"] {
  --color-bg-primary: var(--gray-900);
  --color-bg-elevated: #1e1e1e;
  --color-text-primary: #e0e0e0;
  --color-accent: var(--blue-300);
}

The third layer is component-specific tokens for complex components that need more granular control. Your entire application references only semantic tokens. Switching themes means only the semantic token assignments change.

Respecting system preferences. Use the prefers-color-scheme media query to detect the user’s system preference as the default, then allow manual override:

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --color-bg-primary: var(--gray-900);
    /* ... dark mode tokens */
  }
}

This approach defaults to the system preference but allows the user to explicitly choose light or dark regardless of their system setting.

Transition animations. Add a subtle transition when switching themes: transition: background-color 0.2s ease, color 0.2s ease on the body and major layout containers. Do not apply transitions to all elements globally (it causes performance issues), and keep the duration under 300ms to feel responsive.

Handling Images, Illustrations, and Media

Images are where most dark mode implementations break down. Photos, illustrations, logos, and icons all need different treatment.

Photographs. Photographs generally do not need modification in dark mode. They have their own internal lighting and look natural on dark backgrounds. However, if you display photos with hard edges against the page background, add a subtle 1px border or 4px border-radius to soften the transition between photo and background.

Illustrations and diagrams. Vector illustrations with white or light backgrounds look like bright rectangles punched into a dark interface. You have three options: produce separate light and dark variants of each illustration (best quality, highest effort), apply a CSS filter to reduce brightness and increase contrast (filter: brightness(0.85) contrast(1.1)), or design illustrations with transparent backgrounds and color-neutral palettes from the start (best long-term approach).

Logos. Your logo almost certainly needs a dark-mode variant. If your logo is dark text on a transparent background, it will disappear against a dark background. Prepare a white or light-colored version and switch between them with the theme. Store both variants and reference the appropriate one via CSS or a theme-aware component.

SVG icons. Use currentColor for SVG fill and stroke values. This allows icons to inherit the text color of their parent element, automatically adapting to dark mode without any additional work:

<svg fill="currentColor" viewBox="0 0 24 24">
  <path d="..."/>
</svg>

Shadows and depth. Box shadows are nearly invisible on dark backgrounds. Replace shadows with subtle light borders (1px solid rgba(255, 255, 255, 0.08)) or slightly lighter background colors to indicate elevation. The visual hierarchy should remain clear without relying on shadow visibility.

See also: UI Design Principles for Business Software Applications

Managing User Preferences and Persistence

Giving users control over their theme preference and remembering that choice is essential for a good experience.

Three-state toggle. Offer three options: Light, Dark, and System. “System” follows the operating system preference and should be the default. A two-state toggle (just Light and Dark) frustrates users who want their apps to follow their OS schedule.

Persistence strategy. Store the user’s explicit choice in localStorage for unauthenticated users and in their user profile (server-side) for authenticated users. On page load, apply the theme before the first render to prevent a flash of the wrong theme. For server-rendered applications, this means reading the preference from a cookie (not localStorage, which is not available server-side) and applying the correct class or data attribute during SSR.

Preventing FOUC (Flash of Unstyled Content). The most annoying dark mode bug is a flash of light mode before the dark styles load. Prevent this by inlining a small script in the <head> that reads the theme preference and applies the data attribute before the browser paints. In Next.js, next-themes handles this automatically. For custom implementations, a 10-line blocking script in the head is the standard approach.

Schedule-based switching. Some users prefer dark mode only at night. While this is handled at the OS level for native apps (via “System” preference), web apps can optionally offer a schedule feature. Use the user’s timezone to toggle automatically at sunset/sunrise. The sunrise-sunset.org API provides solar times by latitude/longitude if you want precision, or a simpler approach is letting users set custom start/end times.

Accessibility Considerations in Dark Mode

Dark mode is often framed as an accessibility feature, but a poorly implemented dark mode can make accessibility worse.

Contrast ratios. WCAG 2.1 AA requires a minimum contrast ratio of 4.5:1 for normal text and 3:1 for large text (18px and above or 14px bold). These requirements apply in both light and dark mode. Use tools like the WebAIM contrast checker to verify every text/background combination in your dark palette. Common failures: secondary text that is too transparent, colored text on colored backgrounds, and placeholder text in form inputs.

Focus indicators. The default browser focus ring is often invisible or barely visible on dark backgrounds. Customize focus indicators for dark mode: a 2px solid outline in your accent color with a 2px offset provides consistent visibility. Test keyboard navigation through your entire interface in dark mode.

Color alone as an indicator. If you use red/green color coding for error/success states, ensure you also use icons, text labels, or patterns. This is important in any mode, but dark mode tends to reduce the perceptual difference between colors, making color-only indicators even less reliable for users with color vision deficiencies.

Reduced motion. Some users who enable dark mode do so because of light sensitivity. These users may also benefit from reduced motion. Check prefers-reduced-motion and simplify theme transition animations for users who have enabled it.

Testing protocol. Test your dark mode implementation with a screen reader (VoiceOver on macOS, NVDA on Windows), keyboard-only navigation, and browser zoom at 200%. Dark mode issues often compound with other accessibility needs in ways that are not obvious from visual inspection alone.

Testing Dark Mode Across Platforms and Browsers

Dark mode introduces a combinatorial testing challenge. You need to verify your interface across multiple browsers, operating systems, and system preference states.

Automated visual regression testing. Use tools like Chromatic (with Storybook), Percy, or Playwright’s screenshot comparison to capture every component in both light and dark mode. Run these tests in CI on every pull request. A single missed token reference can break an entire page in one mode while looking fine in the other.

Browser-specific quirks. Safari handles the color-scheme CSS property differently from Chrome in some edge cases, particularly around form input styling. Firefox has its own scrollbar styling behavior in dark mode. Test in all three engines (Chromium, WebKit, Gecko) and document any necessary browser-specific overrides.

Email and embedded content. If your application generates emails or embeds content in third-party contexts (iframes, widgets), dark mode can alter their appearance unexpectedly. Email clients like Apple Mail and Outlook apply their own dark mode transformations. Use the meta name="color-scheme" content="light dark" tag in HTML emails to control this behavior, and test in multiple email clients.

Performance impact. Dark mode itself has negligible performance cost if implemented with CSS custom properties. However, if you are loading different image assets per theme, lazy-load the inactive variants and preload the active ones. Monitor Core Web Vitals (LCP, CLS) in both modes to ensure theme switching does not introduce layout shifts or delayed content.


A well-implemented dark mode shows users that you care about the details of their experience. It requires upfront investment in a proper color system and token architecture, but the result is an interface that feels polished in every lighting condition. If you need help implementing dark mode in your application or building a design system that supports it natively, reach out to our team.

// Related posts