What a palette actually has to do to survive dark mode
Most palettes are picked in light mode and inverted later, which is when the muted text goes illegible and the borders disappear. Dark mode is not a filter over a palette — it is a second set of decisions about the same six roles, and there are only four of them worth getting right.
2026-08-09 · 7 min read
A palette survives dark mode when its six roles were decided twice — once on a light ground and once on a dark one — rather than decided once and inverted. Inversion is what breaks muted text, erases borders, and turns a shadow-based elevation system into a flat sheet of the same colour. None of those failures show up in a swatch strip; all of them show up on a real page.
If you are theming a shadcn build, the six roles are already named for you: --background, --card, --foreground, --muted-foreground, --border, --primary. Here is what each one has to do on the dark side, and how to tell whether it does.
Why inversion fails
The intuition is reasonable: flip the lightness, keep the hue, done. It fails for three separate reasons, and they compound.
Perceived contrast is not symmetric. White text on a dark ground optically "blooms" — it reads heavier than the same contrast ratio does in light mode. A body colour that was comfortable at 60% lightness on white is glaring at 40% on near-black, and a muted colour that was a gentle grey becomes either invisible or shouty depending on which way you rounded.
Borders do the opposite job. In light mode a border is a darker line that separates two light surfaces. Inverted, it becomes a lighter line — which is correct — but the amount is wrong: a border that was a subtle 8% step down from white needs to be a much larger step up from near-black to be visible at all. Palettes that invert linearly lose all their card edges, which is why a lot of dark themes look like one undifferentiated slab.
Shadows stop working. Elevation in light mode is a dark shadow under a light card. On a dark ground there is nothing darker to cast, so the shadow does nothing and the card floats away. Dark modes that read as designed replace shadow-based elevation with surface elevation — the card is slightly lighter than the page, and that step is the whole depth cue.
The four decisions
1. Ground and surface have to differ
Pick --background and --card as two separate values with a real step between them, not the same near-black twice. This is the decision that replaces shadows, and it is the one most inverted palettes skip. A step of roughly 3–5% lightness is usually enough to read as elevation without looking like a mistake.
The corollary is that a true #000000 ground is a trap: it leaves no room below the card, so every surface has to go up, and the page ends up feeling like a stack of grey rectangles. Near-black gives you both directions.
2. Muted text has to clear 4.5:1 against the surface it is actually on
Not against the page — against the card, if that is where it sits. This is the single most common dark-mode contrast failure, and it is invisible in a palette preview because the preview puts muted text on the ground while the product puts it on a card.
Check it on the surface it lives on, in the mode you are checking. Two ratios, not one.
3. Borders get a bigger step in the dark
Whatever step you used in light mode, the dark one needs more. If light-mode --border is a 10% step from --background, dark-mode border wants something closer to 15–20% before a card edge becomes legible on a screen at a normal brightness. Under-stepped borders are why dark themes so often look "soft" in a way nobody asked for.
4. The accent has to be checked in both directions
--primary usually carries text on it (--primary-foreground) and sits as text on a ground (a link, an active tab). Those are two different contrast checks, and a saturated hue that passes one routinely fails the other. A mid-lightness accent that works as a button fill on white is often too dark to read as a link on near-black — it needs to come up, and coming up usually means desaturating a little too, because a fully saturated light hue on a dark ground vibrates.
How to actually check it
Three things, in this order, and none of them is a swatch strip:
- Put the palette on a whole page — ground, card, heading, body, muted caption, border, primary button, secondary button — and look at it. Six colours only tell you anything once they are working against each other.
- Toggle the mode on that same page. Not two screenshots side by side; the same page, flipped, so you catch the thing that moved.
- Check the two ratios that hide: muted-on-card, and accent-as-text-on-ground.
That is also the argument for previewing a palette on your own site rather than a mock. Your site has a specific muted caption in a specific card, and that pair is where the failure lives.
The shortest version
Dark mode is not a filter. It is the same six roles decided a second time, and four of those decisions carry almost all the risk: a real step between ground and surface, muted text checked against the surface it sits on, a bigger border step, and an accent checked as both fill and text.
Every palette in the gallery is rendered as a complete mock page for exactly this reason — swatch strips cannot show you a border failing. And if you want to see one on your own build rather than on ours, the extension applies a palette to a live page and keeps your site's own theme toggle working, so flipping the mode is one click rather than a rebuild.
Q: Can I just use a filter to invert my light theme? A: No. A CSS filter inverts images and shadows along with everything else, and it cannot make the border step bigger or move muted text off a failing ratio. Dark mode needs its own values for the same roles.
Q: Should a dark background be pure black? A: Usually not. Pure black leaves nowhere below your cards, so elevation has to be expressed upward only and the page flattens. Near-black gives you a step in both directions.
Q: What contrast ratio should muted text hit? A: 4.5:1 against the surface it actually sits on — which is the card, not the page background, whenever the text is inside a card. Checking it against the page is the most common way a theme passes a check and fails in use.