---
slug: sample-dark-mode-toggle
title: Dark mode toggle — engineering design
prd: fastpace/docs/prd/sample-dark-mode-toggle.md
status: approved
risk: low
authors: [you@example.com]
created: 2026-04-05
updated: 2026-04-12
references: []
sample: true   # delete this file once you're done test-driving
---

# ERD — Dark mode toggle

> Sample artifact seeded by `fastpace init --sample`. Delete when done.

## Approach

The site already exposes color tokens via CSS variables on `:root`. We
add a `data-theme="light"` attribute on `<html>` that overrides those
variables to a light palette. A small inline script reads
`localStorage.theme` (or `prefers-color-scheme`) before the first paint
to avoid flash-of-wrong-theme.

## Components

| Surface | Change |
|---|---|
| `<head>` inline script | Reads `localStorage.theme` and sets `<html data-theme>` *before* CSS loads. |
| `global.css` `:root[data-theme="light"]` block | New light-mode token overrides. |
| `Base.astro` nav | Adds a `<button class="theme-toggle">` next to the GitHub link. |
| `theme-toggle.client.ts` | Click handler; toggles the attribute and writes `localStorage`. |

## Data flow

1. **First visit**: inline script checks `prefers-color-scheme`. If
   `light`, sets `data-theme="light"`. Otherwise leaves dark (default).
2. **User clicks toggle**: handler flips the attribute and persists
   choice to `localStorage.theme`.
3. **Subsequent visits**: inline script reads `localStorage` first,
   falls back to system preference.

## Risks &amp; mitigations

- **FOUC** — solved by the inline pre-paint script. Verified by adding
  a Playwright test that asserts no theme flash within 200ms of load.
- **Contrast regression** — light-mode tokens audited with axe-core in
  the existing accessibility CI step.
- **localStorage disabled** — falls through to system preference. No
  errors thrown.

## Test plan

- Unit: token-override snapshot test for `:root[data-theme="light"]`.
- Integration: Playwright — toggle click flips theme; reload preserves it.
- A11y: axe-core passes against all routes in both themes.

## Out-of-scope, deferred

- Auto theme (time-of-day). Punt to a follow-up if anyone asks.
- Theming Mermaid/highlight.js — they ship their own themes; revisit
  if dark/light contrast is jarring on docs pages.
