
A dark mode website that reads well starts from a tinted near-black such as #0F1115 rather than #000000, sets body text near 15:1 rather than the maximum 21:1, and shows depth with lighter surfaces instead of shadows. Those three choices, plus the CSS that switches themes without a flash, are most of the craft.
The rest is restraint with color. On a dark ground every saturated hue gets louder, every pure white glows, and every shadow disappears, so a light palette inverted by formula ends up harsh in some places and flat in others. Below are the values that avoid both, with each contrast ratio measured.
What makes a dark palette work#
Five decisions carry a dark page, each backed by Google's or Apple's published guidance.
- A ground above black. Google's Material dark theme codelab builds on
#121212, a dark gray, not black. A cool page can go further and tint the gray:#0F1115isoklch(0.177 0.009 264), a near-black with a trace of blue. - Text below pure white. The same codelab notes that
#FFFFFFwould visually "vibrate" against dark backgrounds, and it sets high-emphasis text at 87% opacity, medium at 60% and disabled at 38%. - Lighter means closer. Material colors raised surfaces with lighter overlays, and Apple's guidelines describe dimmer base colors for background layers and brighter ones for layers above them.
- Accents lightened, not reused. Saturated colors "vibrate" on dark grounds, per Google, so the dark accent is a lighter, softer version of the light one.
- Contrast with a target. Apple asks for at least
4.5:1and recommends striving for7:1for custom colors, especially in small text.
Apple adds a point worth keeping in front of you: dark colors are not necessarily inversions of their light counterparts. Build the dark palette as its own set of values, then map each token to its light twin, and never derive one from the other with a filter.
Dark designs from the library#
The dark designs below share a habit: the ground is never empty black, because the picture supplies the depth.
An emerald-and-gold diver watch on near-black, with a serif headline in soft off-white beside it.
Open the prompt →Free. A huge white word in a dark cavern lit by warm light, and a small diver whose eyes follow your cursor.
Open the prompt →A boxing kangaroo under purple arena light, with a huge yellow wordmark across the bottom of the frame.
Open the prompt →Verdier keeps its ground almost black and lets the emerald and gold of the watch carry the color, the accent discipline of point 4 above. Vesperion is free with an account and shows the other route: a dark scene with warm light in it, so the darkness has texture and the white word has something to sit in front of.
Haymaker shows where a saturated color belongs on dark: at display size. Its yellow is a wordmark set huge across the frame, where strong color reads as energy, while the vibration Google describes is a problem for small text. All three treat dark as a material rather than an absence, which is the difference between a dark design and a light design with its colors flipped.
Two dark palettes you can copy#
Every ratio below was measured with the WCAG formula. Surface 1 is for cards, inputs and the sticky header; surface 2 is for menus, popovers and anything layered over a card.
| Cool role | Hex | On ground #0F1115 | On surface 1 #171A20 | On surface 2 #20242C |
|---|---|---|---|---|
| Text | #E6E8EC | 15.40:1 | 14.21:1 | 12.68:1 |
| Secondary text | #A6ADBA | 8.38:1 | 7.72:1 | 6.89:1 |
| Muted text | #8A919E | 5.96:1 | 5.50:1 | 4.91:1 |
| Accent | #8AB4FF | 9.05:1 | 8.34:1 | 7.45:1 |
| Field border | #6B7383 | 3.66:1 |
| Warm role | Hex | On ground #12100E | On surface 1 #1B1815 | On surface 2 #25211D |
|---|---|---|---|---|
| Text | #EFE9E1 | 15.74:1 | 14.66:1 | 13.25:1 |
| Secondary text | #B3AA9E | 8.28:1 | 7.71:1 | 6.97:1 |
| Muted text | #968D81 | 5.81:1 | 5.41:1 | 4.89:1 |
| Accent | #F2B566 | 10.47:1 | 9.74:1 | 8.81:1 |
| Field border | #77706A | 3.63:1 |
The muted row is the one to check on the lightest surface, because that is where it fails first: both versions stay above 4.5:1 even on surface 2. The field border row exists because WCAG's non-text contrast criterion asks for 3:1 on a border that identifies an input when nothing else does. Decorative dividers can be far quieter, for example rgba(255, 255, 255, 0.08).
A filled button takes the ground color as its text: #0F1115 on #8AB4FF measures the same 9.05:1, since contrast is symmetric. For a warm editorial take on the same idea, the luxury palette uses #EFEBE3 ink on #0B0B0F.
Type on a dark ground#
Light text on a dark ground tends to look heavier and slightly larger than the same text dark on light, so the type needs small adjustments rather than a new scale:
| Setting | Light theme | Dark theme | Why |
|---|---|---|---|
| Body weight, variable face | 400 | 380 | offsets the heavier look of light-on-dark text |
| Body line height | 1.55 | 1.6 | a little more air between bright lines |
| Body size | 18px | 18px | keep the size; change weight and color instead |
| Display tracking | -0.02em | -0.015em | tight display type closes up further when light |
Check the result at the smallest size on the page, usually captions and form hints, on surface 2, since that pairing has the lowest contrast in the palette. The heading ladder itself can stay the same in both themes; the hero section values give one you can reuse.
The mechanics: prefers-color-scheme, color-scheme and light-dark()#
Three pieces of CSS do the switching. The meta tag and the color-scheme property tell the browser the page supports both schemes, which also restyles form controls, scrollbars and the canvas behind the page, per MDN. The light-dark() function then picks a value per token.
<meta name="color-scheme" content="light dark">:root {
color-scheme: light dark;
--bg: light-dark(#FAFAF7, #0F1115);
--surface: light-dark(#FFFFFF, #171A20);
--text: light-dark(#16181D, #E6E8EC);
--muted: light-dark(#5B6170, #A6ADBA);
--accent: light-dark(#2457D6, #8AB4FF);
}
body { background: var(--bg); color: var(--text); }light-dark() returns its second value when the used color scheme is dark, and it only works when color-scheme is light dark, usually set on :root, as MDN explains. It works in Chrome 123, Firefox 120 and Safari 17.5, per caniuse. For older browsers, repeat the dark values inside @media (prefers-color-scheme: dark), which has worked across browsers since January 2020, per MDN.
The light values above were measured too: #16181D on #FAFAF7 is 16.98:1, #5B6170 is 5.93:1, and the light accent #2457D6 is 5.89:1, with white button text on it at 6.16:1. One more switch is worth knowing: per MDN, color-scheme: only light forbids the browser from overriding an element's scheme, which turns off Chrome's Auto Dark Theme for it. Use it on a logo panel or brand area that must never change.
A theme toggle that never flashes#
A manual toggle needs three states, not two: light, dark and "follow the system". With light-dark() tokens, the toggle only has to set one property on the root element, and it must run before the first paint:
<script>
const saved = localStorage.getItem("theme"); // "light", "dark" or null
if (saved) document.documentElement.style.colorScheme = saved;
</script>The control that changes it can be three radio buttons or a select labeled "Theme", with the options System, Light and Dark. Choosing Light or Dark stores the value and sets colorScheme on the root; choosing System calls localStorage.removeItem("theme") and clears the inline style, so the page follows the operating system again.
Inline the script in the <head>, after the color-scheme meta tag and before the stylesheet. MDN recommends placing that meta tag before any CSS to prevent unwanted flashes during load, and an inline script there applies the saved choice before the body renders. A theme applied from a deferred bundle arrives after the first paint, and every visitor who chose dark sees a white frame first.
If the site uses Tailwind, its dark mode documentation shows the same pattern: the dark: variant follows prefers-color-scheme by default, a @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *)); line switches it to an attribute, and an inline head script restores the saved theme from localStorage to avoid the flash. Clearing the stored value returns the page to the system setting.
Images, video and color in the dark#
Pictures need their own rules on a dark page, because a photo with a white background turns into a light box. Apple's guidance is to soften white backgrounds in images by darkening them slightly. Three practical versions:
- Swap the asset. A
<picture>with<source srcset="chart-dark.png" media="(prefers-color-scheme: dark)">serves a dark version of a logo, chart or screenshot. - Dim the rest. A dark-only rule such as
filter: brightness(0.9)on content photographs takes the glare off without changing their color. - Let footage carry the dark. A night film behind the hero gives the ground depth that a flat color cannot.
A night film of a brass automaton among glowing reeds: a dark scene that gives the page its depth.
Open the prompt →A rainbow lorikeet against warm, blurred lights on a dark ground, with a pale serif headline.
Open the prompt →Embercall and Lorikeet both get their darkness from footage, and their color from small lights inside it. For a dark site that wants that effect in the hero, Thanor's animated backgrounds page has a Dark filter of looping clips you can copy by URL into your build; the background guide covers the budgets, and glassmorphism covers frosted panels over a dark field.
When dark should be the default#
Most sites should follow the visitor's setting, and prefers-color-scheme makes that free. A few kinds of site have a case for dark first, with light as the alternative: portfolios and studios whose work is image or film, products that live at night such as music, games and streaming, developer tools whose users often work in dark editors, and luxury pages where the product is lit like an object in a showroom.
Long reading goes the other way. Documentation, editorial pages and anything read for twenty minutes at a time often suit light first, with a dark theme for the visitors who ask for it. A blog that opens dark for everyone overrides every reader whose system is set to light.
Whichever you choose, make the choice once and design both themes with the same care, because a theme added as an afterthought is the one visitors notice. For how film-led, cinematic heroes sit among this year's other directions, see web design trends for 2026, and for the section that closes a dark page, website footer design.
Briefing an AI model for a dark site#
Ask a model for "a dark mode website" and it usually reaches for the defaults of its component kit. The default shadcn/ui theme, which v0's documentation names as its default component system, sets the dark background to oklch(0.145 0 0), about #0A0A0A, and the foreground to about #FAFAFA, per the shadcn theming docs. That pair measures 18.97:1 with zero chroma, a neutral black and white that a great deal of generated work shares. A neutral sitting exactly on the gray axis is the clearest tell of a generated design, because nobody chose it.
The brief has to name the values the kit would otherwise supply:
Theme: dark first, with a light twin. Tokens via light-dark(), color-scheme
light dark on :root, color-scheme meta tag before the CSS.
Dark: ground #0F1115, surface #171A20, raised #20242C, text #E6E8EC,
secondary #A6ADBA, muted #8A919E, accent #8AB4FF, input border #6B7383,
dividers rgba(255,255,255,0.08). No box-shadows: depth by surface steps.
Toggle: system / light / dark, saved in localStorage, applied by an inline
head script. Photos dimmed to brightness(0.9) in dark only.Write the tokens once and let both themes read them; the structure for that is in design tokens for AI websites.
Mistakes that make dark mode look cheap#
#000000with#FFFFFF. The maximum21:1glares, and there is no darker value left for depth.- The light accent reused. A saturated brand blue that reads well on white vibrates on dark. Use a lighter variant.
- Shadows for depth. They vanish on a dark ground. Step the surface lighter and add a hairline border.
- No
color-scheme. Native inputs and scrollbars stay light inside a dark page. - A theme flash. The saved theme applied after first paint shows every returning visitor a white frame.
- Muted text checked on the ground only. Check it on the lightest surface it will sit on.
- Inverted images. A
filter: invert()on photos turns skin and products strange. Swap or dim them instead.
Start from a dark design written as values#
The fastest way to see a dark palette, its surfaces and its motion working together is to open one that already does it. Vesperion is free with an account: a dark cavern, a huge white word, warm light and a small diver whose eyes follow the cursor, with the palette in hex, the type faces and scale, spacing, easing curves and durations written into its prompt.
That prompt runs in Claude, ChatGPT, Gemini, Cursor, Lovable, v0 or Bolt. For the other dark designs in the library, such as Verdier and Embercall, see the plans.
Open Vesperion and copy its prompt; it is free. Compare its hex values with the palettes above, and you will see the same rules applied to a whole designed page.
Questions this raises
What color should a dark mode website background be?
A near-black with a little hue in it, around #0F1115 for a cool page or #12100E for a warm one, rather than pure #000000. Raised surfaces step lighter, for example #171A20 and #20242C. Google's Material guidance uses #121212 as its base dark surface, and Apple's guidelines describe dimmer base colors with brighter colors for layers above them.
Is pure black good for a dark mode website?
Usually not for the page background. Pure black with pure white text reaches the maximum 21:1 contrast, and Google's Material codelab notes that very bright text and saturated colors visually vibrate against dark backgrounds. Pure black also leaves no darker value for depth. A tinted near-black with off-white text around 15:1 reads more calmly.
How do I add dark mode to a website with CSS?
Add a meta tag named color-scheme with the content light dark, set color-scheme: light dark on the root, and define each color token with light-dark(), which picks the second value when the used scheme is dark. For older browsers, repeat the dark tokens inside a prefers-color-scheme: dark media query. A manual toggle then only has to set color-scheme on the root element.
What contrast ratio should dark mode text have?
WCAG asks for at least 4.5:1 for body text and 3:1 for large text, and Apple's guidelines recommend striving for 7:1 for custom colors, especially at small sizes. In practice, primary text around 15:1, secondary text around 7:1 to 8:1 and muted text above 4.5:1 on the lightest surface it sits on gives a dark page clear hierarchy.
How do I stop the white flash when a dark theme loads?
Put the color-scheme meta tag in the head before any stylesheet, so the browser paints a dark canvas from the start, and run a small inline script in the head that reads the saved theme from localStorage and applies it before the body renders. Never apply the saved theme from a script that loads after the page.
Part of
Inspiration, with the prompt
Examples of the looks and effects people search for, from 3D scenes to scroll animation, each one shown working and paired with the specification that builds it.
