Home / Articles / Color /

color

Shadows in dark mode: adjusting for dark backgrounds

Light-mode shadow values don't work on dark backgrounds. The fix involves darker colors, higher opacity, and a different mental model for elevation.

Why light-mode shadows break in dark mode

In a light theme, elevation is communicated by casting a slightly darker shadow below an element — the background is bright, the shadow is a subtle gray, and the contrast reads as "floating above." Switch to a dark background and the same shadow values become nearly invisible. A box-shadow with oklch(0 0 0 / 0.1) on a background of oklch(0.13 0.02 260) has almost no perceptual contrast.

The problem is that shadows in dark mode need to be darker than the already-dark background. You're pushing toward pure black, and you need higher opacity to make the shadow visible at all.

The fix: darker shadows with more opacity

Where a light theme might use 10–15% opacity, dark mode shadows need 40–70%. The lightness drops close to zero, and the blur radius often increases to create a softer edge that reads as depth rather than a hard outline.

/* light mode */
.card {
  box-shadow: 0 2px 8px oklch(0.3 0.01 260 / 0.12);
}

/* dark mode */
.card {
  box-shadow: 0 4px 16px oklch(0.05 0.01 260 / 0.55);
}

Notice the dark-mode shadow doubles the blur radius and increases opacity from 12% to 55%. The lightness drops from 0.3 to 0.05 — nearly black. The offset also increases slightly because the larger blur needs a bigger offset to remain directional.

Using oklch for perceptually correct shadows

oklch() gives direct control over the lightness channel, making it easier to reason about shadow colors than rgba(). You can see at a glance that oklch(0.05 0.01 260) is near-black with a cool tint, while rgba(0, 0, 0, 0.55) tells you nothing about how the shadow will look against a specific background.

/* subtle ambient shadow — dark mode */
box-shadow: 0 1px 3px oklch(0 0 0 / 0.4);

/* medium elevation — dark mode */
box-shadow: 0 4px 12px oklch(0.05 0.01 260 / 0.5);

/* high elevation — dark mode */
box-shadow: 0 8px 24px oklch(0.03 0.02 260 / 0.65);

Adding a small chroma value (0.01–0.02) with a hue matching your color scheme prevents the shadow from looking pure-black, which can feel flat. A slight cool tint at hue 260 integrates the shadow with a blue-tinted dark UI.

Layered shadows for depth

A single box-shadow rarely looks realistic. Real-world light sources create multiple shadow layers — a tight, darker shadow close to the element and a wider, softer shadow further out. This is true in both light and dark themes, but layering matters even more in dark mode where a single shadow can look like a flat border.

.card--elevated {
  box-shadow:
    0 1px 2px oklch(0 0 0 / 0.5),
    0 4px 8px oklch(0.03 0.01 260 / 0.4),
    0 12px 24px oklch(0.05 0.01 260 / 0.3);
}

The first layer is tight and opaque — it anchors the element to the surface. The second is the main directional shadow. The third is a wide ambient glow. Each successive layer is lighter in opacity but larger in spread, mimicking how light diffuses in the real world.

Switching shadows with custom properties

Hard-coding separate light and dark shadow values across every component doesn't scale. Define shadow tokens as custom properties and swap them at the theme level.

:root {
  --shadow-color: oklch(0.3 0.01 260);
  --shadow-strength: 0.12;
}

@media (prefers-color-scheme: dark) {
  :root {
    --shadow-color: oklch(0.05 0.01 260);
    --shadow-strength: 0.55;
  }
}

.card {
  box-shadow:
    0 1px 2px oklch(from var(--shadow-color) l c h / calc(var(--shadow-strength) + 0.1)),
    0 4px 12px oklch(from var(--shadow-color) l c h / var(--shadow-strength));
}

This pattern centralizes shadow tuning. Adjusting --shadow-strength in one place changes every shadow in the system. The calc() bump on the first layer ensures the tight anchor shadow stays slightly more opaque than the diffuse shadow.

Practical strategy for 2026

box-shadow with oklch() colors is supported in Chrome 111+, Safari 15.4+, Firefox 113+ — Baseline widely available. The relative color syntax used in the token example (oklch(from ...)) requires Chrome 122+, Safari 18+, Firefox 128+. For maximum compatibility, use static oklch() values rather than the from syntax.

Audit your shadow values in dark mode by taking a screenshot and desaturating it to grayscale. If cards and surfaces lose all visible separation, your shadow opacity is too low.