Toolium

Box Shadows That Look Right: Layered Shadows and the One-Light Rule

Hemanth Gedda6 min read

The difference between a UI that feels natural and one that feels off is sometimes a single CSS property: box-shadow. Done well, shadows create the illusion of depth that makes cards feel like cards and buttons feel like buttons. Done badly, they make the whole interface look amateurish - shadows pointing in random directions, dark shadows on light backgrounds, hover transitions that look chunky because the shadow does not animate smoothly.

This post is the working knowledge I have built up about box shadows: the syntax, the design rules, the layering trick that Material Design uses, and the specific gotchas that catch developers out.

The syntax, briefly

box-shadow: <x-offset> <y-offset> <blur> <spread> <color>;

Five values. X and Y offset move the shadow away from the element (positive Y goes down, which is the conventional direction). Blur softens the edge (0 = sharp; larger = softer). Spread grows or shrinks the shadow size before the blur applies. Color is the shadow color, usually with some transparency.

An example with concrete values:

box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);

This is a workhorse shadow: 0 horizontal offset (centered), 4 pixels down, 6 pixel blur, 10% black. Subtle, looks like the element is lifted slightly off the page.

The one-light-source rule

In the real world, shadows point away from light. Your eyes are trained to expect this. If every shadow in an interface points in a different direction, your brain notices unconsciously and the design feels off.

The convention: assume light is coming from above and slightly to one side. Shadows go down and to the opposite side. Most design systems pick "light from upper left," which means shadows go down and slightly right. The exact angle is usually small - 0 horizontal offset with positive vertical offset works for most cases.

If you want shadows to feel intentional, use the same direction throughout your interface. If your card shadows go down and right, your button shadows should too. Mixing shadow directions creates visual chaos.

The Material Design layered-shadow trick

Google's Material Design spec popularized the idea that real shadows are not single solid shapes; they are the combination of a sharp close shadow and a soft diffuse shadow. To replicate this in CSS, layer multiple shadows:

box-shadow:
  0 1px 3px rgba(0, 0, 0, 0.12),
  0 1px 2px rgba(0, 0, 0, 0.24);

The first shadow is the soft ambient one. The second is the closer, sharper one. Together they produce a shadow that looks more realistic than either alone.

For higher "elevation" - the Material Design term for how far off the surface an element appears - you scale up both shadows:

/* Elevation 2 */
box-shadow:
  0 3px 6px rgba(0, 0, 0, 0.16),
  0 3px 6px rgba(0, 0, 0, 0.23);

/* Elevation 4 */
box-shadow:
  0 14px 28px rgba(0, 0, 0, 0.25),
  0 10px 10px rgba(0, 0, 0, 0.22);

This is the technique behind every "card lifts on hover" effect you have seen on a modern web app.

Inset shadows: pressed states and recessed elements

Adding "inset" before the values puts the shadow inside the element instead of outside. Useful for:

  • Pressed button states (the button appears recessed).
  • Form fields (a subtle inner shadow makes them feel "depressed" into the page).
  • Subtle outlines that emphasize the boundary of an element.
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);

The hover transition problem

If you transition a box-shadow value, the result is sometimes chunky because the shadow does not redraw smoothly. The browser interpolates the shadow values between the start and end states, but for large changes the intermediate frames can look jarring.

Common fixes:

  • Use a transform instead. Slightly scale the element up on hover. The shadow stays the same; the element itself moves. This animates smoothly because the GPU can handle the transform on its own thread.
  • Use opacity layers. Have two shadow layers - one for resting state, one for hover - and transition the opacity. The hover-state shadow fades in.
  • Use filter: drop-shadow instead of box-shadow. drop-shadow uses the GPU and animates more smoothly. The trade-off is that drop-shadow follows the element's alpha channel (it shadows around irregular shapes), which is usually desirable but occasionally not.

Colored shadows for accent

Modern UI often uses colored shadows that match the element's brand color. A purple button might have a subtle purple shadow under it, giving the impression of a "glow" of the brand color.

box-shadow: 0 8px 24px rgba(124, 58, 237, 0.3);

This works well on dark backgrounds where the colored shadow is visible. On light backgrounds, colored shadows usually disappear into the page and you do not see the effect.

The negative spread trick

You can use negative spread values to make the shadow smaller than the element. Combined with positive offsets, this creates a "shadow that does not show on the sides closer to the light" effect:

box-shadow: 0 8px 12px -4px rgba(0, 0, 0, 0.2);

The -4px spread shrinks the shadow, so even though it has 12px blur, you only see it below the element, not to the sides. This is more realistic for elements that are lifted slightly off a surface.

Common shadow recipes I use

  • Small lift, cards in a list: 0 1px 3px rgba(0,0,0,0.1)
  • Standard card: 0 4px 6px rgba(0,0,0,0.1), 0 2px 4px rgba(0,0,0,0.06)
  • Elevated card or modal: 0 10px 25px rgba(0,0,0,0.15), 0 5px 10px rgba(0,0,0,0.08)
  • Floating action button: 0 8px 16px rgba(0,0,0,0.2)
  • Sticky header dropping a shadow on content below: 0 2px 4px rgba(0,0,0,0.1)
  • Pressed button: inset 0 2px 4px rgba(0,0,0,0.2)

The Toolium Box Shadow Generator has sliders for each parameter, plus presets to start from. Live preview shows the shadow on a sample element; copy the CSS with one click.

Try the tool mentioned in this article

Open tool