← Back to Blog

Dark Mode Done Right: A Design Guide

7 min read

Dark mode is not just "invert the colors." Done poorly, it causes eye strain, hides content, and breaks visual hierarchy. Done well, it is elegant, reduces eye fatigue in low light, and can even save battery on OLED screens.

Choosing a Dark Color Palette

The biggest mistake is using pure black (#000000) as your background. Pure black creates harsh contrast with white text and feels unnatural. Instead, use a dark gray:

<!-- Avoid pure black -->
<div class="bg-black text-white">Too harsh</div>

<!-- Better: dark zinc -->
<div class="bg-zinc-950 text-zinc-50">Much easier on the eyes</div>

<!-- Even better: slightly warm dark -->
<div class="bg-[#0a0a0b] text-zinc-100">Subtle warmth</div>

Build your dark palette from a single base gray scale. Tailwind's zinc scale works well: zinc-950 for backgrounds, zinc-900 for elevated surfaces, zinc-800 for borders, zinc-400 for secondary text, and zinc-100 for primary text.

Contrast Ratios Still Apply

Dark mode does not exempt you from WCAG contrast requirements. Light gray text on dark gray backgrounds can easily fail the 4.5:1 ratio. Test every text color:

  • Primary text (zinc-100 on zinc-950): 15.4:1 ratio. Excellent.
  • Secondary text (zinc-400 on zinc-950): 7.1:1 ratio. Good.
  • Muted text (zinc-600 on zinc-950): 3.3:1 ratio. Fails for body text, okay for large headings only.

Elevated Surfaces

In light mode, you create depth with shadows. In dark mode, shadows are invisible against dark backgrounds. Instead, use lighter background colors for elevated elements:

<!-- Light mode: shadow for depth -->
<div class="bg-white shadow-lg rounded-xl p-6">
  Card content
</div>

<!-- Dark mode: lighter bg for depth -->
<div class="bg-white dark:bg-zinc-900 shadow-lg dark:shadow-none rounded-xl border border-transparent dark:border-zinc-800 p-6">
  Card content
</div>

Each layer up gets slightly lighter: zinc-950 (page) → zinc-900 (card) → zinc-800 (nested element). This creates a clear visual hierarchy without shadows. Our dark pricing component demonstrates this layering approach.

The CSS Variables Approach

Hardcoding dark: variants on every element is tedious and error-prone. A better approach uses CSS custom properties:

:root {
  --background: 0 0% 100%;
  --foreground: 240 10% 4%;
  --card: 0 0% 100%;
  --border: 240 6% 90%;
}

.dark {
  --background: 240 10% 4%;
  --foreground: 0 0% 98%;
  --card: 240 6% 10%;
  --border: 240 4% 16%;
}

Then in Tailwind, reference these variables. Change the theme once and every component updates automatically. This is the approach UI Drop uses across all components.

Component Adaptation Gotchas

Some components need more than a palette swap:

Images. Bright images on dark backgrounds feel jarring. Consider reducing brightness slightly with dark:brightness-90 or adding a subtle overlay.

Borders. Light mode borders (border-zinc-200) become invisible on dark backgrounds. Use dark:border-zinc-800 explicitly.

Status colors. Green, red, and yellow indicators need different shades in dark mode. A bright green that works on white looks neon on dark backgrounds. Desaturate slightly.

Charts and graphs. Data visualization colors often need a complete rethink for dark mode. Lighter, more saturated colors work better on dark backgrounds.

Common Mistakes

Forgetting scrollbar styling. A bright white scrollbar on a dark page is distracting. Use scrollbar-color in CSS or style the webkit scrollbar.

Not testing transitions. If your theme toggle animates, make sure it does not flash white during the switch. Load the theme preference before rendering — UI Drop uses a script in <head> to prevent flash of incorrect theme.

Ignoring system preference. Respect prefers-color-scheme: dark as the default when users have not made an explicit choice.

Browse our components in dark mode to see these principles applied. Try the dark cinematic hero and the glass card for good examples of dark-mode-first design.

Related Articles