Dark Mode Retrofit
Add consistent dark mode to an existing website or fix partial/broken dark mode
Dark mode is a binary commitment -- partial dark mode, where some components flip and others stay white, is worse than no dark mode at all. The human eye notices inconsistency before it notices darkness, and a single white component on a dark page creates a visual jarring that undermines the entire experience. When adding dark mode, the only acceptable outcome is complete coverage across every component, every page, and every state.
## Key Points
1. Extract all colors into CSS custom properties
2. Create light and dark value sets
3. Add a theme toggle and persistence
1. Find every component that doesn't respect dark mode
2. Replace hardcoded colors with variables
3. Ensure consistency
1. Audit every color pairing for contrast
2. Fix problematic mappings
3. Handle images, shadows, and borders
- [ ] Every page background is dark (no white flashes)
- [ ] All text is readable (contrast check)
- [ ] All inputs have visible borders and text
## Quick Example
```tsx
// BEFORE
<div className="bg-white border border-gray-200 text-gray-900">
// AFTER
<div className="bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 text-gray-900 dark:text-zinc-100">
```
```css
[data-theme="dark"] pre, [data-theme="dark"] code {
background: var(--bg-tertiary);
/* Or use a dedicated code theme */
}
```skilldb get web-polish-skills/Dark Mode RetrofitFull skill: 297 linesDark Mode Retrofit
Core Philosophy
Dark mode is a binary commitment -- partial dark mode, where some components flip and others stay white, is worse than no dark mode at all. The human eye notices inconsistency before it notices darkness, and a single white component on a dark page creates a visual jarring that undermines the entire experience. When adding dark mode, the only acceptable outcome is complete coverage across every component, every page, and every state.
The correct approach to dark mode is CSS custom properties with semantic naming, not per-component dark: class proliferation. When values are defined once as variables and remapped in a dark theme context, changes propagate automatically and consistently. The alternative -- adding dark variants to every className in every component -- is a maintenance burden that guarantees future inconsistency as new components are added without their dark counterparts.
Dark mode is not color inversion. It is a thoughtful remapping of a semantic color system where backgrounds get darker, text gets lighter, and elevation is expressed through lightness rather than shadow. Getting this right requires understanding that near-black is better than pure black, near-white is better than pure white, and higher elevation means lighter backgrounds -- the exact opposite of light mode's shadow-based depth model.
You are a theming specialist who adds or fixes dark mode in existing websites. Partial dark mode — where some components flip and others stay white — is worse than no dark mode at all. You make dark mode complete, consistent, and correct.
Assessment: Where Are You Starting?
Scenario A: No Dark Mode Exists
The site uses hardcoded colors everywhere. You need to:
- Extract all colors into CSS custom properties
- Create light and dark value sets
- Add a theme toggle and persistence
Scenario B: Partial Dark Mode
Some components have dark: classes, others don't. Some CSS variables exist, some values
are hardcoded. You need to:
- Find every component that doesn't respect dark mode
- Replace hardcoded colors with variables
- Ensure consistency
Scenario C: Dark Mode Exists but Looks Bad
Values are technically set but colors are wrong — text is unreadable, contrast is poor, elements disappear against backgrounds. You need to:
- Audit every color pairing for contrast
- Fix problematic mappings
- Handle images, shadows, and borders
Implementation Strategy
Step 1: Theme Architecture
/* Use CSS custom properties with data attribute switching */
:root {
color-scheme: light dark;
/* Light mode (default) */
--bg-primary: #ffffff;
--bg-secondary: #f4f4f5;
--bg-tertiary: #e4e4e7;
--bg-elevated: #ffffff;
--text-primary: #18181b;
--text-secondary: #52525b;
--text-tertiary: #a1a1aa;
--text-inverse: #ffffff;
--border-default: #e4e4e7;
--border-strong: #d4d4d8;
--shadow-sm: 0 1px 2px rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px rgb(0 0 0 / 0.07);
--shadow-lg: 0 10px 15px rgb(0 0 0 / 0.1);
--primary-500: #3b82f6;
--primary-600: #2563eb;
}
[data-theme="dark"] {
--bg-primary: #09090b;
--bg-secondary: #18181b;
--bg-tertiary: #27272a;
--bg-elevated: #1c1c1e; /* Cards/modals sit slightly above background */
--text-primary: #fafafa;
--text-secondary: #a1a1aa;
--text-tertiary: #71717a;
--text-inverse: #18181b;
--border-default: #27272a;
--border-strong: #3f3f46;
/* Shadows are more subtle in dark mode */
--shadow-sm: 0 1px 2px rgb(0 0 0 / 0.2);
--shadow-md: 0 4px 6px rgb(0 0 0 / 0.3);
--shadow-lg: 0 10px 15px rgb(0 0 0 / 0.4);
/* Primary colors shift slightly for dark backgrounds */
--primary-500: #60a5fa;
--primary-600: #3b82f6;
}
Step 2: Theme Toggle
// hooks/useTheme.ts
export function useTheme() {
const [theme, setTheme] = useState<'light' | 'dark'>(() => {
if (typeof window === 'undefined') return 'light';
return localStorage.getItem('theme') as 'light' | 'dark'
|| (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
});
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
}, [theme]);
const toggle = () => setTheme(t => t === 'light' ? 'dark' : 'light');
return { theme, toggle, setTheme };
}
// components/ThemeToggle.tsx
import { Sun, Moon } from 'lucide-react';
export function ThemeToggle() {
const { theme, toggle } = useTheme();
return (
<button
onClick={toggle}
className="p-2 rounded-md text-gray-500 hover:bg-gray-100 dark:hover:bg-zinc-800 transition-colors"
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
>
{theme === 'light' ? <Moon className="h-5 w-5" /> : <Sun className="h-5 w-5" />}
</button>
);
}
Step 3: Component-by-Component Migration
For each component, replace hardcoded colors with CSS variables:
/* BEFORE */
.card {
background: white;
border: 1px solid #e5e7eb;
color: #111827;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
/* AFTER */
.card {
background: var(--bg-elevated);
border: 1px solid var(--border-default);
color: var(--text-primary);
box-shadow: var(--shadow-sm);
}
Or with Tailwind (if using dark: variant):
// BEFORE
<div className="bg-white border border-gray-200 text-gray-900">
// AFTER
<div className="bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 text-gray-900 dark:text-zinc-100">
The CSS variable approach is better for retrofits because you change values in one place
instead of adding dark: to every className in every component.
Step 4: Handle Special Cases
Images:
/* Reduce image brightness slightly in dark mode */
[data-theme="dark"] img:not([data-preserve-color]) {
filter: brightness(0.9);
}
/* Invert dark logos/icons for dark mode */
[data-theme="dark"] .logo-dark {
display: none;
}
[data-theme="dark"] .logo-light {
display: block;
}
Shadows: Dark mode shadows should be darker and more diffuse. They're sitting on a dark background, so the light-mode subtle shadow becomes invisible.
Borders: Borders become MORE important in dark mode (they define containers where shadows can't), but must use darker values to avoid looking harsh.
Code blocks:
[data-theme="dark"] pre, [data-theme="dark"] code {
background: var(--bg-tertiary);
/* Or use a dedicated code theme */
}
Input fields:
[data-theme="dark"] input, [data-theme="dark"] select, [data-theme="dark"] textarea {
background: var(--bg-secondary);
border-color: var(--border-default);
color: var(--text-primary);
}
Charts and data visualization:
/* Charts need adjusted colors for readability on dark backgrounds */
[data-theme="dark"] .chart-line { stroke: var(--primary-400); }
[data-theme="dark"] .chart-grid { stroke: var(--border-default); }
[data-theme="dark"] .chart-label { fill: var(--text-secondary); }
Dark Mode Color Rules
| Element | Light Mode | Dark Mode | Principle |
|---|---|---|---|
| Page background | White (#fff) | Near-black (#09090b) | Don't use pure black — it's harsh |
| Card background | White (#fff) | Slightly lighter (#18181b) | Elevated surfaces get lighter in dark mode |
| Modal background | White (#fff) | Lighter still (#1c1c1e) | Higher elevation = lighter |
| Text primary | Near-black (#18181b) | Near-white (#fafafa) | Don't use pure white — it's harsh |
| Text secondary | Medium gray (#52525b) | Medium gray (#a1a1aa) | Both need to pass contrast |
| Borders | Light gray (#e4e4e7) | Dark gray (#27272a) | Visible but subtle |
| Primary color | 600 shade | 400-500 shade | Lighter on dark backgrounds |
| Shadows | Subtle (5-10% opacity) | Heavier (20-40% opacity) | Need more punch on dark |
The Elevation Principle
In dark mode, higher elements are LIGHTER (opposite of light mode where shadows create depth):
Background: #09090b (darkest)
Card: #18181b (slightly lighter)
Modal: #1c1c1e (lighter still)
Dropdown: #27272a (even lighter)
Tooltip: #3f3f46 (lightest)
Testing Dark Mode
Visual Scan Checklist
- Every page background is dark (no white flashes)
- All text is readable (contrast check)
- All inputs have visible borders and text
- All buttons are visible and have hover states
- Cards and containers are distinguishable from the background
- Modals have dark backgrounds (not white on dark page)
- Images are not blindingly bright
- Charts and graphs are readable
- Focus rings are visible on dark backgrounds
- Scrollbars are styled for dark mode (or use overlay scrollbars)
- Third-party embeds don't create white rectangles
Flash Prevention
Prevent the "white flash" on page load before theme is applied:
<!-- Add to <head> before any CSS -->
<script>
(function() {
var theme = localStorage.getItem('theme');
if (!theme) {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
Anti-Patterns
- Don't use pure black (#000000) for backgrounds. It's too harsh. Use near-black (#09090b).
- Don't use pure white (#ffffff) for text on dark. Use near-white (#fafafa).
- Don't invert colors programmatically.
filter: invert(1)creates garbage. - Don't forget about third-party widgets (auth forms, chat widgets, embeds).
- Don't ship partial dark mode. If half the components are dark and half are white, it looks worse than no dark mode at all.
- Don't use the same shadow values in both modes. Dark mode needs heavier shadows.
Install this skill directly: skilldb add web-polish-skills
Related Skills
Color System Repair
Fix color chaos in a vibecoded website — too many near-duplicate colors, inconsistent
Component Unification
Take scattered, inconsistent UI components from a vibecoded website and unify them into a
Design Token Extraction
Extract a unified set of design tokens (colors, typography, spacing, shadows, radii) from a
Form Elements
Unify all form elements — inputs, selects, textareas, checkboxes, toggles, radio buttons,
Modal Dialog System
Unify all modals, dialogs, drawers, sheets, and overlay patterns across a website into a
Navigation Patterns
Fix inconsistent navigation patterns — mismatched headers, footers, sidebars, breadcrumbs,