← All articles

Designing Shadows That Feel Real

Juniper Hale · 3 min read
Share image
Share image

I have a hunch that the best interfaces share a quality that's hard to name. They feel real. You want to reach out and press the buttons. A big part of that quality comes down to something most of us treat as an afterthought: shadows.

Look around the web and you'll find a sea of fuzzy grey rectangles. In this tutorial, we'll turn those into shadows that feel like they belong to an actual light source.

Two white cards on a lilac background; the right one has a softer, layered shadow
Hover-free comparison: a single shadow (left) versus five stacked ones (right).

Why bother with shadows?

Shadows tell the eye how far an element floats above the page. A dialog floating high above the content should cast a large, soft shadow; a button resting on the surface casts a small, crisp one. When every element uses the same shadow, depth stops meaning anything.

Elevation is information. Treat your shadows like a type scale: a handful of deliberate steps, each one saying “how close is this to the reader?”

A consistent light source

In the real world, light comes from somewhere. If one card's shadow falls down-right and another's falls straight down, the scene feels subtly broken, even if nobody can say why.

The rule is simple: keep the ratio between the horizontal and vertical offsets the same for every shadow on the page.*Usually 1:2 — the light is above and slightly to the left of the reader. As elements rise, the offset grows, the blur grows, and the opacity shrinks.

  1. Every shadow shares the same offset ratio.

  2. Higher elements get larger offsets and blur.

  3. Higher elements get fainter, not darker.

The tricks

Layering

3D renderers trace thousands of light rays to get soft, natural shadows. box-shadow just blurs a rectangle. We can fake some of that richness by stacking several shadows, each a little larger and fainter than the last:

Code PlaygroundCSS
.layered { box-shadow: 0 1px 1px hsl(0deg 0% 0% / 0.075), 0 2px 2px hsl(0deg 0% 0% / 0.075), 0 4px 4px hsl(0deg 0% 0% / 0.075), 0 8px 8px hsl(0deg 0% 0% / 0.075), 0 16px 16px hsl(0deg 0% 0% / 0.075); }

Hover over either card to lift it. The layered one keeps its softness as it rises; the single shadow just slides.

Performance tradeoff. Five shadows means five times the painting work. On modern hardware it rarely matters, but don't animate a layered box-shadow directly — animate transform and let the shadow come along for the ride.

Color-matched shadows

A shadow is the absence of light on a coloured surface. Black at 30% opacity doesn't just darken the background; it desaturates it. Instead, pick a darker, more saturated version of the background hue:

shadows.css
.card {
  /* on a lilac background */
  --shadow-color: 286deg 36% 56%;
  box-shadow:
    0.5px 1px 1px hsl(var(--shadow-color) / 0.7),
    1px 2px 2px hsl(var(--shadow-color) / 0.5),
    2px 4px 4px hsl(var(--shadow-color) / 0.3);
}
A small pale clay creature under a spotlight, casting a soft shadow
Real light: the shadow is soft at the edges and darkest right under the creature's feet.

Putting it all together

Combine the three ideas — one light source, layered shadows, colour-matched hue — into a small set of elevation tokens. Three steps (low, medium, high) cover nearly every component I've ever built.

tokens.css
:root {
  --elevation-low: 0.3px 0.5px 0.7px hsl(var(--shadow-color) / 0.34),
    0.4px 0.8px 1px -1.2px hsl(var(--shadow-color) / 0.34);
  --elevation-medium: /* … */;
  --elevation-high: /* … */;
}

Shadows aren't decoration. They're the cheapest way to tell the reader what they can touch.

Continue the journey

If this clicked for you, the next stop is the transitions guide, where we make those cards move.

0 comments

  • Be the first to comment.