Lab 10: Dark Mode & Theming

Time: 30 minutes | Level: Practitioner | Docker: docker run -it --rm node:20-alpine sh

Overview

Build a complete dark/light theming system using prefers-color-scheme, CSS custom properties as design tokens, the color-scheme property, forced-colors support, modern oklch() color space, and a persistent JS theme switcher.


Step 1: prefers-color-scheme Media Query

/* Base (light) styles */
:root {
  --color-bg:          #ffffff;
  --color-surface:     #f5f5f5;
  --color-text:        #111111;
  --color-text-muted:  #666666;
  --color-primary:     #3b82f6;
  --color-border:      #e0e0e0;
}

/* Dark mode: override tokens */
@media (prefers-color-scheme: dark) {
  :root {
    --color-bg:          #0d0d0d;
    --color-surface:     #1a1a1a;
    --color-text:        #f0f0f0;
    --color-text-muted:  #aaaaaa;
    --color-primary:     #60a5fa;
    --color-border:      #333333;
  }
}

/* Use tokens throughout */
body {
  background: var(--color-bg);
  color: var(--color-text);
}

.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
}

Step 2: color-scheme Property


Step 3: Manual Theme Toggle with Data Attribute


Step 4: oklch() Color Space

oklch(lightness% chroma hue) — perceptually uniform color space:


Step 5: forced-colors (High Contrast Mode)


Step 6: JavaScript Theme Switcher


Step 7: Anti-Flash Script


Step 8: Capstone — Theme Token Generator

📸 Verified Output:


Summary

Feature
Syntax
Purpose

OS preference

@media (prefers-color-scheme: dark)

Auto dark mode

Color scheme hint

color-scheme: light dark

Style system UI

Manual toggle

[data-theme="dark"]

User choice

oklch()

oklch(57% 0.20 250)

Perceptual color

Forced colors

@media (forced-colors: active)

High contrast support

JS set theme

el.setAttribute('data-theme', ...)

Dynamic switching

Persist preference

localStorage.setItem(...)

Remember choice

Anti-flash

Inline script in <head>

Prevent FOUC

Last updated