The Complete Guide to CSS box-shadow
Master CSS shadows from the basics to advanced layering techniques. Learn how offset, blur, spread, and inset interact, and how to create depth without sacrificing performance.
Shadows are how the web borrows depth from the physical world. Used well, they tell users what is interactive, what is elevated, and what is currently focused without a single line of explanation. Used poorly, they make interfaces look soft, blurry, or stuck in 2014. The good news is that the underlying CSS is small enough to learn in a sitting and flexible enough to last a career.
This article walks through every parameter of box-shadow, explains how the values combine, and then steps up to the patterns that production design systems actually ship: layered shadows for realistic elevation, focus rings, neumorphism done responsibly, and shadows that respect dark mode and accessibility.
Anatomy of a single shadow
A box-shadow declaration is a list of up to six values:
box-shadow: [inset] [offset-x] [offset-y] [blur-radius] [spread-radius] [color];Three of those values do most of the visual work:
- Offset (x, y): moves the shadow relative to the element. Positive Y pushes the shadow downwards, mimicking a light source from above. Most UI shadows use a small positive Y and a zero or near-zero X.
- Blur radius: softens the shadow edge. Higher blur values produce diffuse, ambient shadows; lower values produce crisp, pressed-out lines. Blur is where realism lives.
- Spread radius: grows or shrinks the shadow before blur is applied. Negative spread is the secret to subtle inner highlights and constrained shadow shapes.
The inset keyword inverts the rendering: the shadow is drawn inside the element instead of outside. This is how you get pressed-button effects, recessed panels, and the inner glows used in focus indicators.
One shadow is rarely enough
Real-world objects cast more than one shadow. A card sitting on a desk has a tight, dark shadow directly underneath it (contact shadow), and a much larger, softer shadow extending outward (ambient shadow). CSS lets you stack shadows by separating them with commas, and that single feature is what separates a flat-looking card from a believable one.
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.06), /* contact */
0 4px 12px rgba(0, 0, 0, 0.08), /* mid */
0 16px 40px rgba(0, 0, 0, 0.06); /* ambient */The order matters: shadows listed first are painted on top. In practice you list them tightest-to-widest, smallest blur to largest. Together they imitate how light scatters in real environments.
An elevation system you can reuse
Most design systems define elevation as a discrete scale —elevation-1 through elevation-5, or Tailwind's shadow-sm through shadow-2xl — and apply the same shadow at the same level everywhere. The reason is consistency: if every card on the page picks its own shadow, the eye cannot tell which card is more important. A scale forces intentionality.
A workable starting point looks like this:
- Level 1 — flush: single faint shadow, used for dividers and subtle separation from the background.
- Level 2 — resting card: two-layer shadow, used for the default state of cards, panels, and tiles.
- Level 3 — hover/raised: three-layer shadow, used when an element is hovered, focused, or actively dragged.
- Level 4 — popover/dialog: larger ambient shadow plus tighter contact shadow, for floating UI on top of other content.
- Level 5 — modal: the heaviest combination, often paired with a backdrop blur to communicate that the underlying page is now disabled.
Generating these by hand is fine; generating them with a visual tool is faster. The CSS Shadow Generator on ProDevTools.xyz lets you tune offset, blur, spread, and color in real time and exports both raw CSS and Tailwind class names ready to paste into your component library.
Color: the most under-used parameter
The default instinct is to reach for rgba(0, 0, 0, 0.1) and call it done. That works on white backgrounds, but it falls apart on coloured surfaces and dark themes. Two refinements raise the quality bar immediately:
- Tint shadows toward the surface color. A blue card looks more grounded with a deep navy shadow than a pure-black one. Sample a darker variant of the surface color and use it as the shadow base.
- Use HSL or modern color spaces. Working in HSL lets you adjust saturation and lightness independently, which is how you achieve shadows that feel correct on both light and dark backgrounds.
Shadows in dark mode
On a dark background, a black shadow disappears. Designers often respond by lifting the shadow color toward gray, but that just produces a fuzzy halo. The cleaner solution is to switch the elevation metaphor: instead of darker shadows, use lighter highlights on the top edge of an element to suggest that it is closer to the light source. This is how most modern dark UI design systems handle elevation, and it pairs naturally with a subtle, very low-opacity shadow underneath for grounding.
Focus rings done right
Outlines are the right tool for keyboard focus, but box-shadowwith zero offset and a tight spread is a common alternative because it follows the element's border-radius. The accessibility rule is non-negotiable: focus must be visible, with sufficient contrast, even on busy backgrounds. A two-layer focus ring (inner light ring, outer dark ring) ensures the indicator stays visible regardless of the background color behind it.
Performance: shadows are not free
Every box-shadowwith a non-zero blur is rasterised by the browser's compositor. On dense pages — long lists of cards, animated grids — the cost adds up. Three rules keep it manageable:
- Limit large-blur shadows to elements that need them. Heavy shadows on every list row are a common source of jank during scroll.
- Animate
opacityortransform, not the shadow itself. Cross-fading between two pre-rendered shadow layers is cheaper than animating blur or spread directly. - Promote frequently-shadowed elements to their own layer. Apply
will-change: transformsparingly so the browser keeps the shadow rasterisation cached.
When to reach for filter: drop-shadow instead
box-shadow follows the element's border box. filter: drop-shadow()follows the element's actual rendered shape, including transparent regions. Use the latter when you need a shadow on an irregular shape — an SVG icon, a PNG with transparency, a clipped element. The price is that filter can be more expensive on large surfaces, so prefer box-shadow for normal rectangles and reach fordrop-shadow only when the visual problem demands it.
Wrapping up
Most of the difference between an amateur and a professional UI is not custom illustration or exotic typography — it is a quiet, consistent shadow system applied with intent. Pick an elevation scale. Layer at least two shadows for any non-trivial surface. Tint toward the surface color. Test in dark mode. Keep the heavy blurs rare. With those rules, the visual language of your interface becomes legible to users without needing a single tooltip.
If you want a starting point, open the Shadow Generator, build a five-step elevation scale, and drop the exported tokens directly into your component library. From there, the work is just refinement.