Files
singular-particular-space/skills/commissioning-skill/fetched-skills/ux-design/references/refui-theming-dark-mode.md
JL Kruger 5422131782 Initial commit — Singular Particular Space v1
Homepage (site/index.html): integration-v14 promoted, Writings section
integrated with 33 pieces clustered by type (stories/essays/miscellany),
Writings welcome lightbox, content frame at 98% opacity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 12:09:22 +02:00

9.4 KiB

Theming & Dark Mode Design

Creating effective color themes and implementing dark mode correctly.

Dark Mode Philosophy

Dark mode isn't just inverting colors—it requires deliberate design decisions to maintain usability, hierarchy, and aesthetics.

Why Dark Mode Matters

  • User preference: Many users prefer it
  • Eye strain: Reduces strain in low-light environments
  • Battery life: Saves power on OLED screens
  • Accessibility: Some users have photosensitivity
  • Professional expectation: Users expect modern apps to support it

Dark Mode Color Principles

Don't Just Invert

Light Mode Bad Dark Mode Good Dark Mode
White #ffffff Black #000000 Dark gray #18181b
Black text #000000 White text #ffffff Off-white #fafafa
Gray #6b7280 Gray #6b7280 Lighter gray #a1a1aa

Key Principles

1. Use dark grays, not pure black

Pure black (#000000) creates harsh contrast and "halation" (text appears to glow).

/* Background scale for dark mode */
--bg-base: #09090b;      /* Deepest background */
--bg-subtle: #18181b;    /* Cards, elevated surfaces */
--bg-muted: #27272a;     /* Hover states, inputs */
--bg-emphasis: #3f3f46;  /* Active states */

2. Reduce contrast slightly

Max contrast in dark mode is harsher than in light mode.

/* Text colors for dark mode */
--text-primary: #fafafa;   /* ~95% white, not 100% */
--text-secondary: #a1a1aa; /* Muted text */
--text-tertiary: #71717a;  /* Subtle text */

3. Desaturate colors

Bright saturated colors on dark backgrounds cause eye strain.

/* Light mode brand color */
--primary-light: #3b82f6;  /* Bright blue */

/* Dark mode - slightly desaturated */
--primary-dark: #60a5fa;   /* Lighter, less saturated */

4. Elevate with lightness, not shadow

In dark mode, shadows are invisible. Show elevation with lighter surfaces.

/* Light mode: shadow for depth */
.card-light {
  background: white;
  box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}

/* Dark mode: lighter surface for depth */
.card-dark {
  background: #27272a;  /* Lighter than base */
  box-shadow: none;     /* Or very subtle */
}

Building a Dark Mode Palette

Step 1: Define Your Gray Scale

Create 9-10 shades from near-black to near-white:

/* Dark mode gray scale (Zinc example) */
--gray-950: #09090b;  /* Deepest background */
--gray-900: #18181b;  /* Card backgrounds */
--gray-800: #27272a;  /* Elevated surfaces */
--gray-700: #3f3f46;  /* Borders, dividers */
--gray-600: #52525b;  /* Disabled states */
--gray-500: #71717a;  /* Placeholder text */
--gray-400: #a1a1aa;  /* Secondary text */
--gray-300: #d4d4d8;  /* Primary text (alt) */
--gray-200: #e4e4e7;  /* Headings */
--gray-100: #f4f4f5;  /* Emphasis text */
--gray-50:  #fafafa;  /* Primary text */

Step 2: Adjust Accent Colors

/* Primary color adjustments */
/* Light mode: use 500-600 range */
--primary-light: #2563eb;

/* Dark mode: use 400-500 range (lighter) */
--primary-dark: #3b82f6;

/* Same for semantic colors */
--success-light: #16a34a;
--success-dark: #22c55e;

--error-light: #dc2626;
--error-dark: #ef4444;

Step 3: Define Semantic Tokens

/* Semantic tokens that switch based on mode */
:root {
  --color-bg: var(--gray-50);
  --color-bg-subtle: var(--gray-100);
  --color-text: var(--gray-900);
  --color-text-muted: var(--gray-600);
  --color-border: var(--gray-200);
  --color-primary: var(--blue-600);
}

[data-theme="dark"] {
  --color-bg: var(--gray-950);
  --color-bg-subtle: var(--gray-900);
  --color-text: var(--gray-50);
  --color-text-muted: var(--gray-400);
  --color-border: var(--gray-800);
  --color-primary: var(--blue-400);
}

Implementation Strategies

Strategy 1: CSS Custom Properties

:root {
  --bg: #ffffff;
  --text: #18181b;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #18181b;
    --text: #fafafa;
  }
}

body {
  background: var(--bg);
  color: var(--text);
}

Strategy 2: Data Attribute + Class

<html data-theme="dark">
[data-theme="light"] {
  --bg: #ffffff;
}
[data-theme="dark"] {
  --bg: #18181b;
}
// Toggle theme
function toggleTheme() {
  const current = document.documentElement.dataset.theme;
  document.documentElement.dataset.theme = current === 'dark' ? 'light' : 'dark';
  localStorage.setItem('theme', document.documentElement.dataset.theme);
}

// Initialize from preference
function initTheme() {
  const saved = localStorage.getItem('theme');
  const preferred = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  document.documentElement.dataset.theme = saved || preferred;
}

Strategy 3: Tailwind Dark Mode

<!-- With class strategy -->
<html class="dark">
  <body class="bg-white dark:bg-zinc-950 text-zinc-900 dark:text-zinc-50">
// tailwind.config.js
module.exports = {
  darkMode: 'class', // or 'media' for system preference only
}

Component Considerations

Cards and Surfaces

/* Light: white with shadow */
.card {
  background: white;
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}

/* Dark: lighter surface, subtle or no shadow */
[data-theme="dark"] .card {
  background: var(--gray-900);
  box-shadow: 0 1px 3px rgba(0,0,0,0.3); /* Darker shadow if any */
  /* Or: border: 1px solid var(--gray-800); */
}

Form Inputs

.input {
  background: white;
  border: 1px solid var(--gray-300);
}

[data-theme="dark"] .input {
  background: var(--gray-900);
  border: 1px solid var(--gray-700);
}

Buttons

/* Primary button */
.btn-primary {
  background: var(--primary);
  color: white;
}

[data-theme="dark"] .btn-primary {
  /* Often same or slightly adjusted */
  background: var(--primary-dark);
}

/* Secondary button */
.btn-secondary {
  background: var(--gray-100);
  color: var(--gray-900);
}

[data-theme="dark"] .btn-secondary {
  background: var(--gray-800);
  color: var(--gray-100);
}

Images and Media

/* Reduce brightness/contrast of images in dark mode */
[data-theme="dark"] img:not([data-no-dim]) {
  filter: brightness(0.9) contrast(1.1);
}

/* Invert diagrams/illustrations if needed */
[data-theme="dark"] .diagram {
  filter: invert(1) hue-rotate(180deg);
}

Syntax Highlighting

Don't forget code blocks need dark mode variants:

  • Use dark theme variants of syntax highlighters
  • Or invert colors appropriately
  • Popular: One Dark, Dracula, Night Owl

Testing Dark Mode

Checklist

  • All text is readable (sufficient contrast)
  • Hierarchy still clear (headings vs body)
  • Focus states visible
  • Images don't blow out
  • Forms inputs clearly visible
  • Error/success states distinct
  • Loading states visible
  • Shadows/elevation still work
  • Icons visible (may need color swap)
  • Brand colors still recognizable

Contrast Ratios

Same WCAG requirements apply:

  • Normal text: 4.5:1 minimum
  • Large text: 3:1 minimum
  • UI components: 3:1 minimum

Common dark mode fails:

  • Gray text on dark background
  • Colored text on colored backgrounds
  • Disabled states too subtle

Theme Toggle UI

Placement

  • Header/navigation (most common)
  • Settings page
  • Footer (less common)

Icon Patterns

<!-- Sun/Moon toggle -->
<button aria-label="Toggle dark mode">
  <svg class="sun hidden dark:block">...</svg>
  <svg class="moon block dark:hidden">...</svg>
</button>

State Options

  1. Light / Dark - Simple toggle
  2. Light / Dark / System - Respect OS preference option
  3. Auto only - Always follow system (no toggle)

Persistence

// Save preference
localStorage.setItem('theme', 'dark');

// Load preference (with system fallback)
const theme = localStorage.getItem('theme') ||
  (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');

Advanced: Multiple Themes

Brand Themes

[data-theme="brand-a"] {
  --primary: #ff6b6b;
  --primary-hover: #ee5a5a;
}

[data-theme="brand-b"] {
  --primary: #4ecdc4;
  --primary-hover: #3dbdb5;
}

Theme Structure

/* Base tokens (don't change) */
--spacing-4: 16px;
--radius-md: 8px;

/* Color tokens (change per theme) */
--color-primary: ...;
--color-bg: ...;

/* Component tokens (reference color tokens) */
--button-bg: var(--color-primary);
--card-bg: var(--color-bg);

Common Mistakes

Mistake Problem Fix
Pure black background Harsh, looks flat Use dark gray (#18181b)
Pure white text Too much contrast Use off-white (#fafafa)
Same saturated colors Eye strain Desaturate for dark mode
Shadows for elevation Invisible in dark Use lighter surfaces
Forgetting images Can be too bright Dim images slightly
One contrast check Colors interact differently Check all combinations
Forgetting focus states Invisible borders Ensure visible focus rings

Quick Reference

Minimum Viable Dark Mode

:root {
  --bg: #ffffff;
  --bg-subtle: #f4f4f5;
  --text: #18181b;
  --text-muted: #71717a;
  --border: #e4e4e7;
  --primary: #2563eb;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #18181b;
    --bg-subtle: #27272a;
    --text: #fafafa;
    --text-muted: #a1a1aa;
    --border: #3f3f46;
    --primary: #3b82f6;
  }
}

body {
  background: var(--bg);
  color: var(--text);
}