| name | dark-mode-and-theming |
| description | Use when building, adding, or auditing a dark mode — or any multi-theme (light/dark/high-contrast/dimmed) system — for a website, app/web UI, dashboard, or themed document. Builds a true dual-mode semantic palette where dark is a deliberate remap (lowered chroma, lightened/desaturated accents, dark-grey surfaces carrying the brand hue, NOT |
| metadata | {"portable":true,"category":"02-color-brand-and-visual-identity","compatible_with":["claude-code","codex"]} |
Dark Mode And Theming
Acknowledgement: Shared by Peter Bamuhigire, techguypeter.com.
Use When
- Adding a dark mode to an artifact that already has (or is getting) a light palette, or
building light + dark together from the start.
- Designing a multi-theme system (light, dark, dimmed, high-contrast) that must share one set
of semantic roles and switch cleanly via
prefers-color-scheme or a manual toggle.
- Auditing or fixing a dark mode that was produced by inverting the light theme and now reads
as muddy, vibrating, washed-out, or flat (no sense of depth).
- Defining the dark-side values of design tokens — the dark column of a light/dark token pair.
Do Not Use When
- You have no base palette yet and need to choose the anchor, primary hue, and light ramp
first — start at
02-color-brand-and-visual-identity/color-system-and-palette, then return
here for the dark remap. This skill builds on that one; it does not re-derive the palette.
- The only question is whether a specific pair passes contrast or which contrast tool to use —
use
02-color-brand-and-visual-identity/accessible-color-and-contrast.
- The task is the token architecture/naming/export (tiers, JSON, Style Dictionary), not the
dark values themselves — use
09-design-systems-tokens-and-theming/design-tokens-and-naming
and feed it the role→value map this skill produces.
Required Inputs
| Input | Supplied by | Required? | Why |
|---|
| Light-theme semantic tokens | Colour-system workflow | yes | Provides roles to remap |
| Brand hue, assets, and elevation model | Brand owner | yes | Preserves identity and depth |
| Supported themes and preference rules | Product brief | yes | Defines switching behaviour |
- The existing (or just-built) light semantic role map and tonal ramp, ideally in OKLCH —
surface, raised/sunken surfaces, text (primary + muted), border, accent, and the
status set success / warning / error / info.
- The brand hue and its chroma, so dark surfaces can carry a trace of it rather than going
neutral grey or pure black.
- Which themes are required (dark only, or also dimmed / high-contrast) and how they switch
(system
prefers-color-scheme, manual toggle, or both).
- The smallest text size and thinnest UI stroke the dark values must survive (drives the
contrast gate), and whether elevation is expressed (cards, menus, modals, popovers).
Workflow
-
Treat dark as a remap, never an inversion. Do not flip lightness (L → 1−L) or invert
the hex values. Inversion produces over-bright, over-saturated, vibrating colour and breaks
semantic meaning (your "danger" red can land on a near-white that no longer reads as danger).
Build the dark column role by role from the brand, exactly as color-system-and-palette
step 8 directs — this skill is that step, expanded. State up front that you are remapping.
-
Anchor dark surfaces in a brand-tinted dark grey, not #000. Pure black surfaces with
light text over-contrast and smear (halation) — text appears to bleed, especially at small
sizes and for readers with astigmatism. Use a near-dark grey carrying a trace of the brand
hue: in OKLCH, base surface around L 0.14–0.20, very low chroma (C ≈ 0.01–0.03) on
the brand hue. This keeps the dark theme feeling authored in the brand, not a default void
(Mission §0, doctrine/design-doctrine.md). See references/dark-mode-semantic-roles.md.
-
Lower chroma across the whole dark palette. Saturated colours that read as confident in
light mode glow and buzz against dark surfaces (simultaneous contrast / chromatic aberration
at the eye). Pull chroma down for surfaces, borders, and especially large filled areas. The
accent and status colours keep more of their chroma than surfaces but still less than their
light-mode values — see step 4. This is the single biggest tell of a real dark mode versus a
naive one.
-
Lighten and slightly desaturate accents and status colours so they hold meaning at depth.
A mid/dark accent that passed 4.5:1 on a light surface will fail badly on a dark one. Raise
each accent's and status colour's lightness (and trim its chroma a little) until it clears the
gate against the dark surface it actually sits on — typically the light-mode L moves up by
~0.15–0.30 in OKLCH. The hue is preserved so error still reads red, success still green;
only L and C move. This is where "not just inversion" becomes concrete.
-
Express elevation with surface lightness, not drop shadows. In light mode, raised
surfaces use shadow; on dark surfaces shadows are nearly invisible, so the convention flips:
higher elevation = lighter surface. Build an elevation ramp where base/sunken is darkest
and each raised layer (card → menu → dialog → popover/toast) steps L up by ~0.02–0.04.
Keep chroma low and constant so the steps read as depth, not as a different colour. Optionally
add a faint top border (a 1px highlight) instead of a shadow to imply a raised edge.
Material's "elevation overlay" is the same idea; implement it as explicit surface tokens.
Anti-Patterns
- Inversion —
filter: invert(), flipping lightness, or swapping hex pairs. The textbook
slop dark mode: over-bright, vibrating, semantically broken.
#000 surfaces and pure-white (#fff/L 1.0) body text — both over-contrast and smear
(halation). Use brand-tinted dark grey and ~L 0.92–0.96 text.
- Full-chroma accents carried straight over from light mode — they glow and buzz; lower
chroma and raise lightness instead.
- Shadows for elevation on dark — invisible; use lighter surfaces (and optional top
highlight) to signal depth.
- Skipping the dark contrast re-gate because the light theme passed — the most common real
accessibility regression a dark mode ships.
- Maxed-out white text assumed to be "more accessible" — it is less comfortable; parity is
about equal comfort, not maximum ratio.
- Muddy or illegible warning/amber in dark — verify the whole status set, not just text.
- Duplicating components per theme instead of swapping token values behind shared roles.
Outputs
| Output | Consumer | Evidence / acceptance |
|---|
| Theme role mapping | Token and component owners | Every semantic role maps across modes |
| Dark elevation and asset spec | Designers and engineers | Surfaces, logos, imagery, and charts defined |
| Theme validation record | Accessibility and QA | Contrast, states, preference, persistence, and flash tested |
Quality Standards
- Treat dark mode as a contextual composition, not an inversion filter.
- Retest every foreground/background and interaction state in every theme.
- Stop release for flashing, unreadable, unthemed, or identity-breaking surfaces.
Decision Rules
| Condition | Decision | Wrong-choice failure |
|---|
| Brand accent vibrates on dark | Reduce chroma or raise lightness | Accent blooms and loses legibility |
| Surface needs elevation | Increase lightness within tinted neutrals | Hierarchy flattens |
| OS preference exists and no choice is saved | Honour the OS preference | Theme ignores user expectation |
| Asset lacks a suitable dark variant | Provide treatment or block | Identity becomes illegible |
Capability Contract
Read and rendered theme inspection are required. Editing needs implementation authority; preference persistence and deployment remain with engineering. Do not destructively alter master brand assets.
Degraded Mode
If required evidence or tooling is unavailable, use the scoped fallback below and mark the result unverified.
Without rendering, deliver a conditional role map and state matrix, marking adaptation unverified. Recover with the light theme when dark assets or accessible pairs cannot be established.
- A dual-mode semantic role map (light + dark columns, OKLCH + hex), a brand-tinted dark surface
and dark ramp, an elevation-by-lightness ramp, lightened/desaturated accents and a verified
status set, recorded WCAG results for both modes, and the theme-switch mechanism
(
prefers-color-scheme + manual toggle) — handed off to token export.
Examples
examples/light-dark-token-pair.md — a complete worked OKLCH light and dark token set for
a real anchor, with the elevation ramp, the accent/status remap, and the contrast checks for
both modes. Not lorem; a buildable spec.
References
02-color-brand-and-visual-identity/color-system-and-palette — the entry skill that selects
the anchor, primary hue, light ramp, and semantic roles; build on it, do not re-derive.
This skill is its step 8 ("dark mode is a deliberate remap, not an inversion") fully expanded.
02-color-brand-and-visual-identity/accessible-color-and-contrast — the contrast tool
(WCAG vs APCA), non-colour status cues, and colour-blind-safe ramps the status set relies on.
references/dark-mode-semantic-roles.md — the role → light value / dark value mapping with
OKLCH targets and the elevation ramp.
doctrine/references/wcag-2.2-criteria.md — the contrast floor (§1.4.3, §1.4.11) re-applied to
dark, and the "design with APCA, certify with WCAG" method note. Hard gate.
doctrine/design-doctrine.md — Mission §0 (authored over convergent; a brand-tinted, crafted
dark theme is the moat, the inverted default is the slop), Anti-Slop Charter §2 (state the
remap choice before producing the artifact).
- Human authority (named for provenance, not citable in-repo): Josef Albers, Interaction of
Color (simultaneous contrast — why saturation must drop on dark); Material Design elevation
overlay model (elevation as surface lightness).