Dark Mode Retrofit
Add consistent dark mode to an existing website or fix partial/broken dark mode
Dark Mode Retrofit
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.
Related Skills
Color System Repair
Fix color chaos in a vibecoded website — too many near-duplicate colors, inconsistent
Component Unification Specialist
Take scattered, inconsistent UI components from a vibecoded website and unify them into a
Design Token Extractor
Extract a unified set of design tokens (colors, typography, spacing, shadows, radii) from a
Form Element Unification
Unify all form elements — inputs, selects, textareas, checkboxes, toggles, radio buttons,
Modal & Dialog Unification
Unify all modals, dialogs, drawers, sheets, and overlay patterns across a website into a
Navigation Pattern Unification
Fix inconsistent navigation patterns — mismatched headers, footers, sidebars, breadcrumbs,