Why one shadow reads as fake and three read as real
An object on a surface casts three shadows at once: a tight dark contact shadow beneath it, a mid shadow offset away from the light, and a wide faint ambient shadow from light bouncing around the room. One layer has one offset and one blur, so it can be any of the three, not all. The Popover preset gives each layer one job: 0 1px 2px at 4%, 0 4px 8px -2px at 6%, 0 12px 24px -6px at 10%, for 20% total, less ink than most single-layer shadows.
Negative spread is the part people leave out. Spread grows the shadow before it is blurred, so a 24px blur with no correction bleeds sideways as far as it bleeds down, and the element reads as floating in fog. Pulling spread in by about a quarter of the blur keeps the wide layer tucked under the element.
Why the shadow colour should not be black
Compositing pure black over a coloured surface pulls it towards neutral grey, so the shaded region reads colder and less saturated than the area beside it. On a near-white page the difference is small; on a brand colour or warm paper it is what makes a card look dirty rather than lifted. Use a dark tone of the surface hue instead.
Dark mode is sharper: a black shadow on a near-black background is invisible, since nothing is darker than the page. Dark-mode elevation needs a higher alpha, a colder tone than the surface, or a hairline border instead.
The Tailwind form
The output is the arbitrary-value form, shadow-[0_1px_2px_rgb(11_28_30_/_0.06)], with underscores for spaces. Multiple comma-separated layers work and so does inset inside the brackets, but the compiler rewrites each colour to var(--tw-shadow-color, …), so a shadow-* colour utility on the same element overrides the whole stack. The arbitrary-property form [box-shadow:…] skips those variables entirely.
Common problems
- The shadow is cut off. An ancestor with
overflow: hiddenclips it. Move the shadow to the clipping element, or usefilter: drop-shadow()on the parent. - It follows the rectangle, not the shape.
box-shadowfollows the border box and its radius, so a shadow on a transparent PNG or an SVG icon traces the box.filter: drop-shadow()follows the alpha channel and costs more to paint. - Animating it stutters. A blurred shadow repaints every frame. Put the hover shadow on a pseudo-element and animate its
opacityinstead.
Frequently asked questions
What is the difference between blur and spread?
Blur softens the edge without changing the size. Spread changes the size before blurring: positive grows, negative shrinks. Spread with no blur gives a hard-edged copy of the element, which is how flat long shadows and hard offset borders are built.
Does a box shadow affect layout?
No. It paints outside the border box and takes no space, so it never pushes a neighbour. That is the difference from an outline with an offset, and why a shadow can overflow a scroll container and get clipped.
How many layers is too many?
Three is where the returns flatten, four is a ceiling for a modal. Each layer is another blurred paint pass over the same pixels, so a six-layer shadow on two hundred rows is measurable. Nothing here stops you at a number, though: stack as many as the effect needs and the link will carry all of them.
Why does my shadow look grey instead of soft?
Pure black at low opacity desaturates whatever it falls on, so the shaded area reads colder than the surface beside it. Use a dark tone of the surface hue instead. It is the single change that separates a shadow that looks designed from one that looks defaulted.