Skip to content
Foundations

Dark mode

Not an inversion: surfaces lighten as they rise, shadows collapse, and the status hues are re-derived.

Why it is not a token flip

Every page on this site is already in both themes — use the control at the bottom of the nav. Three things had to move independently for that to be true.

Shadows stop working

A shadow against a dark ground is nearly invisible, and faking it with a heavier black produces a smudge. Elevation moves into the surface: lighter is higher.

The accent cannot survive

#4740DB on charcoal is 1.9:1. Links and icons take the lifted #8C86F0; the filled button keeps a bolder indigo with white text. That is why --gd-accent and --gd-accent-ink are two tokens rather than one.

Status must be re-derived

A tint that reads as caution on white reads as sickly on charcoal. Each hue keeps its identity but gains lightness and loses saturation, so all four clear 4.5:1 on the dark surface.

The surface ladder

Lighter is higher. The basemap goes to #22242B rather than black, so parcel strokes and overlay fills keep the same relationship to their ground that they have in daylight.

surface
--gd-surface#FFFFFF#17181E

the default ground for cards, panels and rows

--gd-surface-page#F4F4F2#101116

the page behind everything — warm off-white in the light, the lowest charcoal in the dark

--gd-surface-subtle#FBFBFC#1D1F26

table headers and glyph-tile grounds: a surface that is not quite the card it sits on

--gd-surface-raised#FFFFFF#1D1F26

a surface lifted off its ground — a floating pill, a popover, a menu. In light the shadow carries it and this equals `surface`; in dark the surface itself has to

--gd-surface-sunken#F4F4F7#101116

recessed groups — segmented tracks, icon tiles, layer lists. Sunken is the one people get wrong: a GROUP of things is recessed, not raised

--gd-surface-hover#FAFAFB#24262E

pointer over a row. Hover is a SURFACE change, never a shadow change — a list that lifts under the pointer shimmers

--gd-surface-pressed#F4F4F7#2B2D36

the moment of the click

--gd-surface-selected#EEF0FF#3B3866

the chosen row or option

--gd-surface-disabled#F0F1F4#212329

an inert control's fill

--gd-surface-inverse#14151A#24262E

toasts, code blocks, the ink CTA band — a surface that is dark in BOTH themes

--gd-surface-solid#14151A#F2F2F4

the neutral high-emphasis CTA fill — near-black on a light page, near-white on a dark one. NOT `surface-inverse`, which stays dark in both

--gd-surface-map#F0EEE9#22242B

the basemap ground. Deliberately not black in the dark, so parcel strokes and overlay fills keep the relationship to their ground that they have in daylight

--gd-surface-scrimrgba(20, 21, 26, 0.42)rgba(0, 0, 0, 0.55)

the dimmer behind modal surfaces — dialog, drawer, palette. Deeper in the dark, because 42% charcoal over a charcoal page barely reads as a veil

--gd-surface-on-accentrgba(255, 255, 255, .10)rgba(255, 255, 255, .10)

a card or row sitting ON a filled accent band

--gd-surface-on-accent-strongrgba(255, 255, 255, .16)rgba(255, 255, 255, .16)

a chip on a filled accent band — one step up from the card it sits in

Re-derived status

success
--gd-success#1B8A4C#8FBF87

success fill — dots, icon tiles, progress bars. Never set type in it AA-nontext on --gd-surface

--gd-success-ink#15703D#8FBF87

the only success value that may carry text. Re-derived for dark rather than lightened — a tint that reads as caution on white reads as sickly on charcoal AA on --gd-surface

--gd-success-wash#F1F7F2color-mix(in srgb, #8FBF87 14%, #17181E)

the success ground — alerts, callouts, washed chips

--gd-success-tint#CFE3D5color-mix(in srgb, #8FBF87 34%, #17181E)

the border of a success wash

--gd-success-on-solid#FFFFFF#14151A

the GLYPH on a filled success tile. Non-text: these tiles carry a 15px icon, never prose — success at 4.4:1 would not clear AA for a sentence AA-nontext on --gd-success

info
--gd-info#2F6FB0#7FB3E6

info fill — dots, icon tiles, progress bars. Never set type in it AA-nontext on --gd-surface

--gd-info-ink#285F96#7FB3E6

the only info value that may carry text. Re-derived for dark rather than lightened — a tint that reads as caution on white reads as sickly on charcoal AA on --gd-surface

--gd-info-wash#EDF3FAcolor-mix(in srgb, #7FB3E6 14%, #17181E)

the info ground — alerts, callouts, washed chips

--gd-info-tint#C5DAEEcolor-mix(in srgb, #7FB3E6 34%, #17181E)

the border of a info wash

--gd-info-on-solid#FFFFFF#14151A

the GLYPH on a filled info tile. Non-text: these tiles carry a 15px icon, never prose — success at 4.4:1 would not clear AA for a sentence AA-nontext on --gd-info

caution
--gd-caution#A8850F#DDB63F

caution fill — dots, icon tiles, progress bars. Never set type in it AA-nontext on --gd-surface

--gd-caution-ink#8A6D09#D9B34A

the only caution value that may carry text. Re-derived for dark rather than lightened — a tint that reads as caution on white reads as sickly on charcoal AA on --gd-surface

--gd-caution-wash#FCF6E6color-mix(in srgb, #DDB63F 14%, #17181E)

the caution ground — alerts, callouts, washed chips

--gd-caution-tint#EBD9A2color-mix(in srgb, #DDB63F 34%, #17181E)

the border of a caution wash

--gd-caution-on-solid#FFFFFF#14151A

the GLYPH on a filled caution tile. Non-text: these tiles carry a 15px icon, never prose — success at 4.4:1 would not clear AA for a sentence AA-nontext on --gd-caution

danger
--gd-danger#C43D3D#E97B7B

danger fill — dots, icon tiles, progress bars. Never set type in it AA-nontext on --gd-surface

--gd-danger-ink#A83232#E97B7B

the only danger value that may carry text. Re-derived for dark rather than lightened — a tint that reads as caution on white reads as sickly on charcoal AA on --gd-surface

--gd-danger-wash#FCEFEFcolor-mix(in srgb, #E97B7B 14%, #17181E)

the danger ground — alerts, callouts, washed chips

--gd-danger-tint#EFCACAcolor-mix(in srgb, #E97B7B 34%, #17181E)

the border of a danger wash

--gd-danger-on-solid#FFFFFF#14151A

the GLYPH on a filled danger tile. Non-text: these tiles carry a 15px icon, never prose — success at 4.4:1 would not clear AA for a sentence AA-nontext on --gd-danger

Selecting a theme

Two hooks, deliberately. The media query follows the operating system unless the user has chosen; the attribute is that choice.

// follow the OS — the default, no attribute at all document.documentElement.removeAttribute('data-gd-theme') // pin it document.documentElement.setAttribute('data-gd-theme', 'dark')

Neither hook is a control. The switch a person actually presses is Theme toggle, which cycles all three choices — and which deliberately stamps nothing before paint, because that is a head concern the app has to add for itself.

An app that ships light-only should say so with color-scheme: light rather than by shipping half a palette. Half a dark theme is a bug report.

Navigate

Esc