DevKit
← Back to Blog

Mastering CSS Box Shadow

8 min read

Shadows are the quietest part of good interface design. A card that floats, a button that looks pressable, a modal that lifts above the page, all of it comes from box-shadow. The property looks intimidating because it crams several numbers into one line, but each number has a clear job.

The syntax, number by number

A single shadow takes up to four length values followed by a color. In order they are: horizontal offset, vertical offset, blur radius, and spread radius.

.card {
  box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.15);
}

Reading that from left to right: the first 0 is the horizontal offset, so the shadow is not pushed left or right. The 4px is the vertical offset, pushing the shadow down. The 12px is the blur radius, which softens the edge. The last 0 is the spread, which would grow or shrink the shadow if it were nonzero. Finally the color, usually a low-opacity black.

Offset, blur, and spread in plain terms

The offsets decide direction. A positive vertical offset drops the shadow below the element, which mimics a light source above, the look our eyes expect. Horizontal offset is usually left at 0 unless you want a side-lit effect.

Blur controls softness. A blur of 0 gives a hard, crisp shadow with a sharp edge. Larger blur values spread the darkness over more pixels, producing the soft diffusion of a real shadow. Most natural shadows use a blur roughly two to three times the vertical offset.

Spread grows or shrinks the shadow before blurring. A positive spread makes the shadow larger than the element, while a negative spread pulls it in. Negative spread paired with a vertical offset creates a tight shadow that only peeks out from one edge, which looks clean on small chips and tags.

Inset shadows

Add the inset keyword and the shadow draws inside the element instead of outside. This is how you suggest a recessed surface, an input field that looks carved into the page, or a button that appears pushed in when active.

.input {
  box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
}

The same four values apply, but now the shadow falls on the inner side of the border. Inset shadows are easy to overdo; keep the opacity low or the field starts to look dirty rather than recessed.

Stacking multiple shadows

One element can carry several shadows at once. List them separated by commas, and the browser paints them in order with the first one on top. This is the single most useful technique for realistic depth, because real shadows are never one flat blob.

.card {
  box-shadow:
    0 1px 2px rgba(0, 0, 0, 0.08),
    0 4px 8px rgba(0, 0, 0, 0.06),
    0 12px 24px rgba(0, 0, 0, 0.05);
}

The tight, dark first layer anchors the element to the surface. The wider, fainter layers below suggest ambient light wrapping around it. Each layer gets softer and more transparent as it spreads, which is exactly how light behaves. A single shadow with high blur never looks as convincing as three stacked ones.

Elevation systems

Design systems formalize this idea into an elevation scale. Instead of inventing shadow values per component, you define a handful of levels, each a stacked shadow, and reuse them. Low levels sit near the surface, high levels float well above it.

:root {
  --elevation-1: 0 1px 2px rgba(0,0,0,0.1);
  --elevation-2: 0 2px 4px rgba(0,0,0,0.1), 0 4px 8px rgba(0,0,0,0.08);
  --elevation-3: 0 4px 8px rgba(0,0,0,0.1), 0 12px 24px rgba(0,0,0,0.08);
}

.menu {
  box-shadow: var(--elevation-3);
}

Storing shadows in custom properties keeps the whole interface consistent. A resting card uses elevation 1, a hovered card jumps to elevation 2, and an open dropdown reaches elevation 3. The jump in shadow communicates the change in height, which is the entire point of elevation.

box-shadow versus drop-shadow

There is a second way to cast a shadow: the drop-shadow filter. The two look similar but behave differently. A box-shadow always traces the rectangular box of the element, including its border-radius. A filter: drop-shadow follows the actual visible shape, including transparent regions of a PNG or the outline of an SVG icon.

.icon {
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
}

Use box-shadow for cards, buttons, and anything rectangular, because it is cheaper to render and supports spread and inset. Reach for drop-shadow when the element has a non-rectangular silhouette, like a logo with cutouts or an icon with transparency, where a box shadow would awkwardly outline the invisible bounding rectangle. Note that drop-shadow has no spread value and no inset mode.

Practical defaults

A few values cover most situations. Subtle card lift: 0 1px 3px rgba(0,0,0,0.1). Floating element: 0 10px 30px rgba(0,0,0,0.15). Pressed button: inset 0 2px 4px rgba(0,0,0,0.2). Start from those, then adjust the blur and opacity until the depth matches the height you want the element to feel at. Shadows read best when they are quiet, so when in doubt, lower the opacity.

Related tools: