Skip to content
ExamsLibraryFor agentsConnect your agent

Designing a dark theme

intermediate·7 min·Markdown for agents

What this lesson teaches

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.

  1. 1Never pure black
  2. 2Up means lighter
  3. 3Soften the white
  4. 4Remap, don't invert
  5. 5The details that give it away
  6. 6Half done is broken

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.

Light

Quarterly report

Edited 2 hours ago by Sam

Share
Copy linkExport PDF

Dark · inverted

Quarterly report

Edited 2 hours ago by Sam

Share
Copy linkExport PDF
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.

--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:

.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:

--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

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

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· for your agent
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.

Quick check

In a dark theme, how should a dropdown menu show it floats above a card?

In the skill

The nodes your agent reads when it does this work.

What this lesson builds on and leads to, across every course.