UI/UX Design

Dark Mode UI: Best Practices for Dual Theme Design

A
Abdul Rahaman
13 September 2026
10 min read
dark modeUI/UX designCSS variablesdesign systemsdual themeFigma

Dark Mode UI: Best Practices for Designing Dual Themes

Dark mode is not a colour inversion. That is the single most important thing to understand before you build it, and the single most common mistake teams make when they rush it out the door.

Inverting your light palette — turning #FFFFFF into #000000 and calling it done — produces something that technically responds to the system preference but looks jarring, breaks accessibility contrast ratios, and often makes branded elements unrecognisable. The users who switch to dark mode notice. They notice immediately.

This post covers what actually needs to change between a light and dark theme, how to architect a token system that makes dual themes maintainable, and the specific visual traps that catch teams building dark mode for the first time.


Why Dark Mode Is Harder Than It Looks

When a user switches to dark mode, they're not just asking for less brightness. They're asking for a coherent visual environment that respects the same hierarchy, readability, and brand identity as the light version — just without the white surfaces that cause eye strain in low light.

The problem is that many design decisions made in light mode don't translate directly to dark. Shadows to create depth disappear on dark surfaces. Colours that pop on white look washed out or harsh on dark grey. Thin typography at 400 weight that reads cleanly on a light background becomes hard to distinguish on #1A1A1A.

This is not a corner case. [VERIFY: Approximately 80% of smartphone users now use dark mode at least some of the time.] If your product doesn't handle it properly, a large share of your users are seeing a broken version of your design every time they open the app in the evening.


Build a Token System, Not a Colour Override

The most maintainable approach to dual theming is a semantic token layer between your raw design decisions and your components. Every colour in your UI is referenced by its purpose, not its value.

Instead of writing:

.card {
  background-color: #FFFFFF;
  color: #1A1A1A;
  border: 1px solid #E5E7EB;
}

You reference semantic tokens:

.card {
  background-color: var(--color-surface);
  color: var(--color-text-primary);
  border: 1px solid var(--color-border);
}

Then define the token values per theme:

:root {
  --color-surface: #FFFFFF;
  --color-text-primary: #111827;
  --color-text-secondary: #6B7280;
  --color-border: #E5E7EB;
  --color-accent: #6366F1;
}

[data-theme="dark"] {
  --color-surface: #1E1E2E;
  --color-text-primary: #E2E8F0;
  --color-text-secondary: #94A3B8;
  --color-border: #2D2D3F;
  --color-accent: #818CF8;
}

When a designer needs to change the surface colour in dark mode, there's exactly one place to change it. No hunting through component files. No regression testing whether #FFFFFF still appears somewhere it shouldn't.

This is the same principle that makes a light-mode palette maintainable — semantic naming decouples the "what" from the "how." Dark mode just adds a second "how." If you're using Tailwind CSS, this maps cleanly to a dark: variant system, but the token abstraction still matters — hard-coding dark:bg-gray-900 in every component creates a different kind of sprawl.


How Contrast Ratios Change on Dark Backgrounds

WCAG AA requires a 4.5:1 contrast ratio between body text and its background, and 3:1 for large text. These ratios apply identically in dark mode. But which colour combinations pass changes significantly.

A colour that reads clearly on white — say, a mid-blue accent at #3B82F6 — may fail contrast requirements on a dark surface. Check every text colour against its dark-mode background before shipping. The WebAIM Contrast Checker handles this in seconds.

Two common failures:

1. Placeholder text. Placeholder text is styled at reduced opacity or a muted colour to indicate it's not real content. That muting which looks subtle on white can drop the contrast ratio below 3:1 on a dark background, making the placeholder unreadable for users with low vision.

2. Accent-on-dark. Brand accent colours are usually chosen to contrast against white. On #1A1A1A, the same accent may either wash out or produce a harsh glow effect. Most palettes need a distinct dark-mode accent — slightly lighter, slightly less saturated — to maintain the right visual weight without glare.

Design the dark palette as its own palette, not a derived one. It shares semantic intent with the light palette but not necessarily the same hex values.


Elevation Without Shadows: The Surface Lightness Approach

In light mode, depth is communicated with shadows. A card sits above the background because it casts a shadow. This works because shadows are dark — they contrast against light surfaces.

On a dark background, dark shadows disappear. They still exist in the CSS, but they're visually inert against a #1A1A1A surface.

Google's Material Design specification describes this problem precisely: in dark themes, elevation is communicated through surface lightness, not shadow depth. The higher an element is in the visual stack, the lighter its surface colour.

Elevation LevelLight ModeDark Mode Surface
Page background#F9FAFB#121212
Card / Surface#FFFFFF + soft shadow#1E1E2E
Raised element (dropdown)#FFFFFF + larger shadow#252535
Modal / Overlay#FFFFFF + strong shadow#2D2D3F
Tooltip / Toast#FFFFFF + strongest shadow#363649

Define your dark surfaces as a graduated scale — four or five steps from the base background to the highest-elevation component. The exact values will depend on your brand hue, but the principle holds: more prominent surfaces are lighter in dark mode, not more shadowed.


How to Handle Icons, Images, and Illustrations

Photographs and complex imagery need no intervention — they carry their own colours and read fine on dark backgrounds. But icons, illustrations, and branded graphics require explicit dark-mode handling.

Icons built with SVG fills that reference hard-coded dark colours (fill="#111827") will become invisible on a dark background. The fix is currentColor, which makes the icon inherit the text colour of its context:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
  <path fill="currentColor" d="M12 2..." />
</svg>

When --color-text-primary switches from #111827 to #E2E8F0, the icon switches with it — no additional dark-mode rule required.

Illustrations are more complex. Brand illustrations often use tones that assume a white canvas. The pragmatic solution: design two versions in Figma, or build the illustration on a transparent background with all tones mid-range, so it reads on both surfaces.

Logos — particularly those with black wordmarks — need a light-mode and dark-mode variant. Make sure both variants are in your design system and that the component consuming the logo renders the correct one based on theme.


If you want to see how a dual-theme design system gets built for a production product — from token architecture through Figma to code — the StartupSphare design team works through this during the Design & Prototype phase of every project.


Theme Switching: The Implementation Details That Trip Teams Up

Detect System Preference

CSS handles this natively with the prefers-color-scheme media query:

@media (prefers-color-scheme: dark) {
  :root {
    --color-surface: #1E1E2E;
    --color-text-primary: #E2E8F0;
    /* ... */
  }
}

This gives you system-respecting dark mode without any JavaScript — a good baseline.

Allow User Override

Most products should let users override the system setting. Someone might want their phone in dark mode but your app in light mode. Store the preference in localStorage and apply it as a data-theme attribute on the <html> element on page load, before the first render:

<!-- In <head>, before any CSS loads -->
<script>
  const saved = localStorage.getItem('theme');
  const system = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  document.documentElement.setAttribute('data-theme', saved || system);
</script>

Loading this inline in <head> prevents the flash of wrong theme (FOWT) — the half-second white flash that appears before a React component mounts and sets the theme via JavaScript after paint. This is the most common dark mode implementation bug and the one users notice most viscerally.

Transition Smoothly

A raw colour switch on theme toggle looks abrupt. A short transition makes it feel polished:

:root {
  transition: background-color 200ms ease, color 200ms ease;
}

Keep it under 250ms. Longer than that and the transition starts to feel sluggish rather than intentional.


The Dark Mode Audit Checklist

Before shipping a dark theme, verify each of these:

  • All colours reference semantic tokens — no hard-coded hex values in component files
  • Body text contrast ratio ≥ 4.5:1 against dark background (WCAG AA)
  • Placeholder text contrast ratio ≥ 3:1 against dark background
  • Accent colour passes contrast on dark surface (may need a distinct dark-mode value)
  • Elevation communicated through surface lightness scale, not shadows
  • All SVG icons use currentColor or have explicit dark-mode fill variants
  • Logo renders the correct variant (light wordmark on dark, dark wordmark on light)
  • System preference detected via prefers-color-scheme
  • User override stored in localStorage and applied before first paint
  • No flash of wrong theme on page load or hard refresh
  • Theme toggle transition ≤ 250ms
  • All form elements (inputs, selects, checkboxes) styled correctly in both themes
  • Tested on iOS in system dark mode and Android in system dark mode

FAQ: Dark Mode for Founders and Product Teams

Should we launch with dark mode on day one, or add it later?

If your design system uses semantic tokens from the start, dark mode is roughly 20–30% more work at the design and front-end layer — not a separate project. Building with tokens also makes your design system significantly more maintainable regardless of theming. For an early MVP where speed matters, shipping with semantic tokens but only a light theme is a reasonable trade-off — the foundation makes adding dark mode later far cheaper than a retrofit.

Why do some colours need different values in dark mode, not just inverted?

Human vision perceives colour differently against dark backgrounds. Pure white (#FFFFFF) on black (#000000) creates a visual vibration effect called halation — the text appears to bleed or glow at the edges, making it harder to read than a slightly off-white (#E2E8F0) on a very dark grey (#121212). Highly saturated colours that pop on white look excessively bright and harsh on dark surfaces. The adjustments are perceptual, not mathematical, which is why a colour inversion produces a technically-correct but visually uncomfortable result.

How do we handle user-generated content and external embeds in dark mode?

External embeds — YouTube players, social widgets, third-party iframes — render in whatever theme their source dictates, often light mode regardless of your settings. The best mitigation is containment: a wrapper surface slightly lighter than your background, so an embedded light-mode widget doesn't create a jarring full-white rectangle in an otherwise dark page. User-generated text inherits your theme's text colour if it doesn't carry its own inline styles. Images are theme-neutral.

Is dark mode a meaningful accessibility feature?

For some users, yes — specifically those with photophobia, certain migraines, and some low-vision conditions where high-contrast dark interfaces are easier to read than bright ones. Dark mode alone doesn't make a product accessible. The WCAG contrast requirements apply equally in dark mode, and some dark mode implementations actually reduce accessibility by lowering contrast in the name of aesthetics. Apply the same accessibility audit to your dark theme that you apply to your light one.

How does dark mode affect brand recognition?

Most brand colours need adjustment in dark mode to maintain their intended visual weight and emotional register. Define your dark-mode brand palette as deliberately as you define the light-mode one — same semantic intent, adjusted values. The goal is not to make the product look different in dark mode, but to make it feel the same in a changed visual environment. This is the same logic behind having different logo variants for print, digital, light backgrounds, and dark backgrounds.


Dark mode built properly — token-based, contrast-tested, with elevation handled through surface lightness — is a quality signal. Users who default to dark mode have seen enough broken dark themes to notice when one is built with genuine care. That impression carries directly into how they think about the rest of the product.

If you're building a product where design quality is part of the value proposition, talk to the StartupSphare design team. We design dual-theme systems from the ground up — not bolted on after launch.


Suggested internal links: UI/UX Design · Custom Software & SaaS · How to Design for Accessibility · Contact Author: Abdul Rahaman Last updated: September 2026

Ready to Build This for Your Business?

Talk to our product team — we'll scope your idea and turn it into web, mobile, or custom software, then help it grow.

Start a Project