Dark Mode Converter
Build dark mode the way design-mature products do: as a second theme with its own logic, not the light theme with colors flipped. Inversion fails because dark UIs work differently -- elevation is communicated by lighter surfaces instead of shadows, saturated colors vibrate on dark backgrounds, and pure black plus pure white is glare, not contrast.
Workflow
- Tokenize first. Inventory every color in the codebase. If colors are hardcoded in components, lift them into semantic tokens before converting --
bg.base,surface,surface.raised,text.primary,text.muted,border,accent, states. A dark mode built on raw hex replacements breaks on the next feature. (If tokens exist, audit for strays with the same sweep.) - Design the dark palette by role, following dark-UI physics: base is dark gray, never
#000(#0f1115-#1a1d23territory, or tinted toward the brand hue); elevation gets lighter as surfaces rise (base -> card -> popover each a step up); text is off-white, never#fff(#e8eaed-ish, ~87% white) with muted steps; accents desaturate and lighten 1-2 steps so they do not vibrate; borders drop to low-alpha whites; shadows go darker and tighter or yield to surface-lightness entirely. Verify every text/surface pair at WCAG AA minimum. - Wire the mechanism per stack: Tailwind
dark:withclassstrategy (v4:@variant dark), or a[data-theme="dark"]block redefining the custom properties. Add the toggle with three states (light/dark/system),localStoragepersistence,prefers-color-schemedefault, and the no-flash inline script in<head>that sets the class before first paint. - Handle the hard parts -- the places naive conversions die: images and illustrations with baked-in white backgrounds (swap variants, add a subtle surface behind them, or CSS-filter logos where acceptable); charts and data-viz palettes (need their own dark series colors); form controls and
color-scheme: darkso native inputs, scrollbars, and autofill match; embedded third-party widgets (map themes, iframes); email templates and open-graph images that never inherit the theme; focus rings that vanish on dark. - Audit pass. Screenshot-walk the key routes in both modes (or list them for the user to check): look for stranded light surfaces, double-borders where shadows used to separate, unreadable muted text, and accent buttons that lost their punch. Output
dark-mode-report.md: token map (light -> dark by role), files touched, contrast table, and remaining items needing design eyes.
Rules
- Semantic tokens or nothing.
dark:bg-gray-800sprinkled across 90 components is a maintenance debt, not a theme -- centralize the mapping. - Never pure black base, never pure white text. Reserve those for moments, not defaults.
- Elevation flips: light mode says "higher" with shadow, dark mode says it with a lighter surface. Convert the logic, not just the colors.
- Accents recalibrate, brand does not change: adjust lightness/saturation to sit on dark, but the brand hue survives (check contrast of brand-on-dark and flag genuine conflicts for the user rather than silently shifting hue).
- Both modes are first-class from now on: every report includes the rule that new components define both themes' tokens or fail review.
- Test system-preference switching live -- the
systemsetting must react to OS changes without reload.
Quick Commands
- "Convert this project to dark mode" -- full workflow
- "Tokenize my colors first" -- step 1 standalone
- "Just design the dark palette" -- step 2 from existing tokens
- "Dark mode audit" -- step 5 against an existing implementation