Dark Mode Retrofit

Visual Arts & Design Advanced web-polish-skills universal
0 Upvotes
4 Views
0 Downloads
1,185 Words

Description

The Dark Mode Retrofit skill adds or fixes dark mode on existing websites, delivering a complete, consistent, and accessible theme.

When to Use

I need to retrofit dark mode on an existing site. | Partial dark mode exists and isn't consistent. | Convert hardcoded colors to CSS variables for dark mode. | Add a theme toggle with persistence. | Audit color contrast for dark mode.

Use Cases

Add full dark mode to an existing site. | Replace hardcoded colors with CSS vars for dark mode. | Add a theme toggle with persistence. | Audit and fix dark mode contrast and readability.

SKILL.md Content

---
name: dark-mode-retrofit
description: "The Dark Mode Retrofit skill adds or fixes dark mode on existing websites, delivering a complete, consistent, and accessible theme."
metadata:
  tags: "visual-design, dark-mode, css-variables, theming, color-contrast, ui-consistency, web-accessibility"
  source: "https://skilldb.dev/skills/web-polish-skills/dark-mode-retrofit"
  pack: "web-polish-skills"
  category: "Visual Arts & Design"
---

# Dark Mode Retrofit

## When to use this skill
Use when the user says things like:
- "I need to retrofit dark mode on an existing site."
- "Partial dark mode exists and isn't consistent."
- "Convert hardcoded colors to CSS variables for dark mode."
- "Add a theme toggle with persistence."
- "Audit color contrast for dark mode."


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:
1. Extract all colors into CSS custom properties
2. Create light and dark value sets
3. 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:
1. Find every component that doesn't respect dark mode
2. Replace hardcoded colors with variables
3. 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:
1. Audit every color pairing for contrast
2. Fix problematic mappings
3. Handle images, shadows, and borders

## Implementation Strategy

### Step 1: Theme Architecture

```css
/* 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

```tsx
// 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:

```css
/* 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):

```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">
```

**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:**
```css
/* 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:**
```css
[data-theme="dark"] pre, [data-theme="dark"] code {
  background: var(--bg-tertiary);
  /* Or use a dedicated code theme */
}
```

**Input fields:**
```css
[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:**
```css
/* 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:

```html
<!-- 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.