# Designing a dark theme

> Build a dark theme from the same color scale as the light one, with near-black surfaces, softened text, lighter layers for depth and calmer accents.

Module: Color, depth and surfaces · Level: intermediate · 7 min · https://school.agents.org.in/design/surface/dark-mode

This course is light only, so here is a dark theme in a box. It starts the way most generated dark modes start. Flip the four switches, one at a time.

*[Interactive demo: Start broken, fix one thing at a time. The light screen beside it uses the same scale.]*

The first version is the light theme run through an inverter: pure black, pure white, shadows that vanish into nothing, and a blue that buzzes. Each switch is one of the skill's rules.

## Never pure black

Pure `#000000` under pure `#ffffff` is harsh. Surfaces feel cheap, and on OLED phones scrolling text smears. Use a near-black with a slight hue, like `#18181b`.

```css
--bg-base:     #18181b; /* the page */
--bg-elevated: #1f1f23; /* +1: cards */
--bg-overlay:  #2a2a2f; /* +2: menus, dialogs */
```

Keep `#000` for photo backdrops, video players and OLED themes that opt in on purpose.

## Up means lighter

In light mode, elevation is a shadow. On a dark page there is nothing dark enough for a shadow to fall on, so **in dark mode, higher surfaces are lighter**, and the topmost layer is the lightest.

A raised card swaps its shadow stack for a faint white ring:

```css
.dark { --elevation-raised: 0 0 0 1px rgb(255 255 255 / .09); }
```

Rings are for elevation only. **Dividers stay solid** dark grays, around `#2a2a2a` on `#18181b`: table rules, list separators, input edges. A see-through white line glows.

## Soften the white

Full white text on near-black is still glaring. White at 90–93% reads softer:

```css
--text-primary:   rgba(255, 255, 255, 0.92);
--text-secondary: rgba(255, 255, 255, 0.62);
```

There's a catch. Text with alpha is only safe on **one known surface**. Over a lighter card, an image or a tinted panel, the alpha mixes with whatever is underneath and the contrast changes. Everywhere else, use solid tokens checked against their real background.

## Remap, don't invert

The deeper idea: dark mode is the same color ramp with the roles pointed at mirrored steps. The page used step 50 in light; it uses 950 in dark. Text used 900; it uses 50. Borders swap 200 for 800.

  **Inverted (avoid):**

Every color flipped by a filter or by hand-picking a second palette. Yellows turn muddy, the accent vibrates, and the two themes drift apart.

  **Remapped (do this):**

One scale. Semantic tokens point at different steps in a `.dark` block. Every pair is then re-checked for contrast.

Because OKLCH lightness is measured, mirrored steps stay just as distinct after the flip. Then two adjustments:

- **Desaturate the accent 20–30%.** A brand blue that looks crisp on white buzzes on black.
- **Re-measure every pair.** The swatch looking right isn't the same as the screen reading right.

This is the payoff of the token tiers from the palette lesson: components never change, only the semantic layer does. *(Per-component `dark:` overrides scatter the theme across the codebase and rot on the first palette change.)*

## The details that give it away

- **Icons and logos** with a hard-coded `fill="#000"` vanish in dark. Use `fill="currentColor"` so they follow the text color.
- **Native controls**: set `color-scheme: dark` in the dark theme, or scrollbars and `<select>` menus stay light.
- **Images** get a white 10% inset outline instead of the black one.
- **The phone's status bar**: one `theme-color` per scheme, matched to the top of the page.

## Half done is broken

A dark mode with one white modal, one light dropdown or one black logo on black makes the whole product look broken. The skill treats it as a blocker, not a nit. If the theme isn't finished, don't ship the toggle and don't declare `color-scheme: dark`. That is also why this course has no dark mode: one well-made theme beats two half-made ones.

**Build a dark theme (paste into your agent):**

```text
Add a dark theme by remapping our existing scale, not inverting it:
- Surfaces: near-black (e.g. #18181b), getting lighter as they rise (card, menu, dialog). No pure #000.
- Elevation in dark is a lighter surface plus a ring (0 0 0 1px rgb(255 255 255 / .09)), not shadows. Dividers stay solid grays.
- Text: 92% white primary, 62% secondary, only on one known surface; elsewhere solid tokens.
- Repoint semantic tokens in a .dark block; no per-component dark: overrides.
- Desaturate the accent 20–30%. SVGs use currentColor. Set color-scheme: dark and a dark theme-color.
- Re-check every text pair for contrast and list the results.
```

**Check yourself:** In a dark theme, how should a dropdown menu show it floats above a card?

- [ ] A bigger, darker shadow
- [x] A lighter surface than the card, with a faint white ring
- [ ] Pure black, to stand out
- [ ] A bright accent border

*Why:* Shadows have nothing to fall on in the dark. Higher layers are lighter surfaces, and a low-opacity white ring defines the edge.

## In the skill

- https://school.agents.org.in/skill/surface/dark-mode.md
