How it works
box-shadow takes one or more layers, separated by commas:
box-shadow: [inset] <x> <y> <blur> <spread> <color>;- x / y move the shadow. Positive
ypushes it down, as if the light comes from above. - blur softens the edge.
0gives a hard copy of the box — the "Sharp" preset. - spread grows or shrinks the shadow before blurring. A negative spread with a large
ygives a shadow that peeks out only underneath. - inset draws the shadow inside the box, like a pressed button or an input field.
Why one layer looks fake
A real object casts two shadows at once: a small, dark one where it almost touches the surface, and a large, faint one from the light spreading around it. A single box-shadow can only be one of those — either crisp and close or soft and far — so it looks like a sticker.
Smooth mode stacks several layers instead. Each layer doubles the offset and blur of the previous one, and the total darkness is split between them:
box-shadow:
0 1px 2px rgb(15 23 42 / 0.064),
0 2px 4px rgb(15 23 42 / 0.064),
0 4px 8px rgb(15 23 42 / 0.064),
0 8px 16px rgb(15 23 42 / 0.064),
0 16px 32px rgb(15 23 42 / 0.064);Close layers make the contact shadow, far ones the soft falloff. Adding layers makes the shadow smoother, not darker.
Small things that make shadows look right
- Tint the shadow. Pure black looks grey and dirty on coloured surfaces. A very dark version of the background hue — the default here is slate — blends in.
- Keep the light consistent. Every shadow on a page should fall the same way. Use one "light from" angle everywhere.
- Bigger elevation, softer shadow. Things further from the surface cast wider, lighter shadows. Raise elevation and softness together.
- Dark themes need less. Shadows barely show on dark backgrounds; lighter surfaces and borders do the work there. Check the preview on both surfaces.
Performance
Shadows are painted, and big blurs are expensive to repaint. Animating box-shadow on hover repaints it every frame. The cheaper trick: put the "hover" shadow on a pseudo-element and animate its opacity, which the browser can do on the compositor:
.card { position: relative; box-shadow: var(--shadow-sm); }
.card::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: var(--shadow-lg);
opacity: 0;
transition: opacity 200ms;
}
.card:hover::after { opacity: 1; }