Context
Dark mode done wrong — simple inversion — creates sites that feel harsh, lose depth, and break brand consistency. Proper dark theming requires a separate color strategy where surfaces communicate elevation through lightness, not shadows, and brand colors shift to maintain vibrancy without blinding contrast.
Procedure
- Define the dark surface scale: 3-5 levels from deepest background to highest elevation. Start with a near-black base (HSL lightness 5-8%), add 3-4% lightness per elevation level.
- Remap text colors: primary text to off-white (90-95% lightness, never pure white), secondary to 60% lightness, disabled to 38% lightness.
- Adjust brand colors for dark backgrounds: increase lightness by 10-20%, reduce saturation by 5-10% to prevent neon glare. Test each adjusted color against the dark surface it appears on.
- Remap semantic colors (success, warning, error): these need desaturation on dark backgrounds to avoid visual harshness.
- Replace box-shadows with surface elevation: in dark mode, a "raised" card uses a lighter surface color instead of a shadow (shadows are invisible on dark backgrounds).
- Implement the theme toggle: check
prefers-color-scheme, allow manual override, persist tolocalStorage, apply viaclass="dark"ordata-theme="dark"on<html>. - Test every component in both modes: buttons, cards, forms, tables, code blocks, images. Fix any contrast failures or visual artifacts.
Output Format
# Dark Mode Specification
## Surface Elevation Scale
| Level | Name | HSL | Usage |
|-------|------|-----|-------|
| 0 | base | hsl(225, 25%, 6%) | Page background |
| 1 | surface | hsl(225, 20%, 10%) | Card, sidebar |
| 2 | surface-raised | hsl(225, 18%, 14%) | Dropdown, modal |
| 3 | surface-overlay | hsl(225, 15%, 18%) | Tooltip, popover |
| 4 | surface-hover | hsl(225, 12%, 22%) | Interactive hover |
## Text Colors (Dark Mode)
| Role | HSL | Opacity | Usage |
|------|-----|---------|-------|
| Primary | hsl(0, 0%, 93%) | 1.0 | Headings, body text |
| Secondary | hsl(0, 0%, 60%) | 1.0 | Captions, labels |
| Disabled | hsl(0, 0%, 38%) | 1.0 | Inactive elements |
| Inverse | hsl(0, 0%, 9%) | 1.0 | Text on light surfaces |
## Brand Color Adjustments
| Token | Light Mode | Dark Mode | Reason |
|-------|------------|-----------|--------|
| primary | hsl(221, 83%, 53%) | hsl(221, 90%, 64%) | +11% lightness for visibility |
| secondary | hsl(262, 83%, 58%) | hsl(262, 75%, 68%) | +10% lightness, -8% saturation |
| success | hsl(142, 71%, 45%) | hsl(142, 60%, 55%) | Desaturated to prevent neon |
## Border & Divider
| Token | Light | Dark | Notes |
|-------|-------|------|-------|
| border | hsl(0,0%,90%) | hsl(0,0%,18%) | Subtle separation |
| divider | hsl(0,0%,95%) | hsl(0,0%,12%) | Section breaks |
## Theme Toggle Implementation
1. Check: window.matchMedia('(prefers-color-scheme: dark)')
2. Check: localStorage.getItem('theme')
3. Priority: user choice > system preference > default (dark)
4. Apply: document.documentElement.classList.toggle('dark')
5. Persist: localStorage.setItem('theme', value)
6. Avoid flash: inline script in <head> before render
## Image Handling
- Reduce brightness to 90% in dark mode: filter: brightness(0.9)
- Add subtle dark overlay on hero images
- SVG icons: use currentColor for automatic color switching
- Screenshots: add dark border/shadow to prevent blending into background
QA Rubric (scored)
- Contrast compliance (0-5): all text passes WCAG AA on its dark surface.
- Elevation clarity (0-5): depth hierarchy is readable without shadows.
- Brand consistency (0-5): adjusted colors still feel like the same brand.
- Flash prevention (0-5): no white flash on page load in dark mode.
Examples (good/bad)
- Good: "Base surface hsl(225, 25%, 6%). Card surface is 4% lighter at hsl(225, 20%, 10%). Primary brand color adjusted from 53% to 64% lightness. Text is hsl(0, 0%, 93%) — warm off-white that reduces eye strain. Theme toggle inline script prevents flash."
- Bad: "Dark mode: background #000000, text #FFFFFF, same blue #2563EB. Cards have box-shadows that are invisible. No system preference detection. White flash on every page load."
Variants
- Auto-only mode: no manual toggle — follow system preference only. Simpler implementation for sites where dark mode is a nice-to-have, not a feature.
- Multi-theme mode: support additional themes beyond light/dark (e.g., high-contrast, sepia) using the same token architecture with additional token sets.