Home / Snippets / Color & Theming /

Dark mode shadows

Tinted oklch() shadows with higher opacity and layered ambient glow — box-shadows that actually work on dark backgrounds.

Gray shadow

Pure black/gray shadow — nearly invisible against a dark background.

Tinted shadow

Hue-matched dark shadow with higher opacity — clearly visible on dark.

Layered shadow

Directional + ambient + border ring + inner highlight — full depth illusion.

Widely Supported
colorno-js

Quick implementation

/* HTML: <div class="card">...</div> */

/* ❌ Gray shadow — invisible on dark backgrounds */
.card--gray-shadow {
  box-shadow: 0 4px 16px oklch(0 0 0 / 0.3);
}

/* ✓ Tinted shadow — hue matches the dark background */
.card--tinted-shadow {
  box-shadow: 0 4px 20px oklch(0.12 0.03 260 / 0.7);
}

/* ✓ Layered shadow — full depth illusion on dark */
.card--layered-shadow {
  box-shadow:
    /* tight directional shadow */
    0 2px 6px oklch(0.08 0.02 260 / 0.6),
    /* diffuse ambient shadow */
    0 8px 24px oklch(0.10 0.03 260 / 0.5),
    /* subtle border ring — replaces a real border */
    0 0 0 1px oklch(0.30 0.03 260 / 0.5),
    /* inner top highlight — fake light source */
    inset 0 1px 0 oklch(1 0 0 / 0.04);
}

/* Utility: colored glow for accent cards */
.card--accent-glow {
  box-shadow:
    0 4px 16px oklch(0.52 0.22 265 / 0.25),
    0 0 0 1px oklch(0.40 0.10 265 / 0.4);
}

Prompt this to your LLM

Includes role, constraints, two framework variants, and edge cases to handle.

You are a senior frontend engineer designing a dark-mode-only design system.

Goal: Create a set of box-shadow utility classes that produce visible, realistic depth on dark backgrounds — using tinted oklch() shadows, layered values, and an inner highlight technique.

Technical constraints:
- All shadow colors must use oklch() — never black (#000), rgba(0,0,0), or gray hex values.
- Tinted shadows match the background hue (e.g., dark blue-gray for a blue-gray dark theme) for physical realism.
- Opacity must be higher on dark backgrounds (0.5–0.7) than on light (0.15–0.25) to remain visible.
- Layered shadows should separate into: tight directional, diffuse ambient, border ring (inset 0), and inner highlight (inset 0 1px 0).
- Do not use filter: drop-shadow() — it does not support multiple layers.
- Provide a .shadow--none reset for removing shadows in flat variants.

Framework variant (pick one):
A) CSS utility classes — .shadow-sm, .shadow-md, .shadow-lg, .shadow-glow-accent.
B) CSS custom properties — define --shadow-sm, --shadow-md, --shadow-lg as variables, applied via a single box-shadow: var(--shadow) declaration on the component.

Edge cases to handle:
- Cards over images: shadow competes with the image — use a ring-only variant (0 0 0 1px).
- Hover state: animate box-shadow transition at 0.25s ease for interactive cards.
- Colored glow for status indicators: error (red hue), success (green hue), warning (amber hue) — all in oklch().
- Ensure shadows do not overflow hidden parent containers — document the need for overflow: visible on shadow wrappers.

Return CSS only.

Why this matters in 2026

The most common dark mode mistake is copying light mode box-shadows verbatim — semi-transparent black shadows that are completely invisible against a dark background because dark plus dark equals nothing visible. Physical shadows work on light surfaces because the shadow is darker than the surface; on a dark background you need to rethink the illusion entirely, using tinted dark shadows with higher opacity to create contrast between the card surface and the space below it. The oklch() color model makes this straightforward: you can specify low-lightness, same-hue values that feel like part of the design system rather than arbitrary magic numbers. Designers who master dark mode shadows immediately elevate the perceived quality of their interfaces.

The logic

The tinted shadow technique picks a oklch() color with very low lightness (0.08–0.12) but the same hue as the background, then raises the alpha to 0.5–0.7 to ensure the darker-than-background value is still visible. Layering multiple box-shadow values in a single declaration allows one shadow to simulate the tight directional drop while a second, larger and more diffuse value simulates the ambient bounce light that makes physical shadows look soft. The zero-spread ring — 0 0 0 1px oklch(0.30 0.03 260 / 0.5) — adds a crisp border-like edge that catches the light and separates the card from the background without requiring a border property. The inset 0 1px 0 inner highlight simulates a light source hitting the top edge of the card.

Accessibility & performance

Box-shadows are purely visual decorations and carry no semantic meaning, so there are no ARIA considerations. They do not affect the document flow, hit-testing area, or keyboard navigation order. Performance-wise, box-shadow is a paint property — animating it triggers repaint but not layout recalculation, and modern browsers batch shadow repaints efficiently. For animated hover shadows, adding will-change: box-shadow to interactive cards can promote the element to its own compositor layer, enabling smoother transitions at scale. Users with prefers-reduced-motion: reduce should still see the static shadow; only transitions on the shadow should be suppressed.