DEV Community

Michael Lip
Michael Lip

Posted on • Originally published at zovo.one

The Anatomy of a Good Box Shadow (and Why Most Look Fake)

Most box shadows on the web look wrong. They look like a dark rectangle floating behind a lighter rectangle. They don't look like real shadows. The reason is that developers reach for box-shadow: 0 2px 4px rgba(0,0,0,0.2) and call it done, without understanding the properties that make shadows look natural.

Real shadows have specific characteristics. They're softer the farther the surface is from the background. They have a direction that implies a light source. They're rarely pure black. And they often involve multiple layers. Let me break down each of these.

Offset implies a light source

The first two values in a box-shadow are the horizontal and vertical offsets. These tell the viewer where the light is coming from. A shadow with 0 4px says "light is directly above." A shadow with 4px 4px says "light is coming from the upper-left."

Most design systems assume a top-down light source, so the vertical offset is positive (shadow falls downward) and the horizontal offset is zero or very small. This is the convention Google's Material Design established, and it works because screens are typically viewed from above.

/* Light from directly above */
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);

/* Light from upper-left */
box-shadow: 2px 4px 6px rgba(0, 0, 0, 0.1);
Enter fullscreen mode Exit fullscreen mode

Pick a direction and be consistent across your entire interface. Mixed shadow directions look like your elements are in different rooms with different lighting.

Blur radius controls perceived elevation

The third value is the blur radius. It controls how diffuse the shadow is. A small blur (2-4px) looks like the element is sitting very close to the surface behind it. A large blur (20-40px) looks like the element is floating high above it.

This maps to real-world physics. Hold your hand close to a table under a lamp -- the shadow is sharp. Raise your hand -- the shadow gets softer and larger.

/* Close to surface - card resting on page */
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);

/* Elevated - dropdown menu */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);

/* High elevation - modal dialog */
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
Enter fullscreen mode Exit fullscreen mode

Notice that the offset and opacity also increase with the blur radius. This is the relationship most developers miss. A 40px blur with a 2px offset looks physically impossible.

Spread radius is usually zero

The fourth value is the spread radius. Positive spread makes the shadow larger than the element. Negative spread makes it smaller. Most natural-looking shadows use zero spread.

The main use case for positive spread is creating border-like effects without using actual borders:

/* Shadow that acts like a border */
box-shadow: 0 0 0 2px #3498db;

/* Focus ring */
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.4);
Enter fullscreen mode Exit fullscreen mode

This technique is especially useful for focus styles because it doesn't affect layout (unlike outline-offset and border).

Multiple shadows create depth

The trick to realistic shadows is layering. A single shadow, no matter how well-tuned, tends to look flat. Two or three shadows at different blur levels create the illusion of ambient light and direct light interacting.

/* Single shadow - ok but flat */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);

/* Layered shadows - much more natural */
box-shadow:
  0 1px 2px rgba(0, 0, 0, 0.07),
  0 4px 8px rgba(0, 0, 0, 0.07),
  0 12px 24px rgba(0, 0, 0, 0.07);
Enter fullscreen mode Exit fullscreen mode

The first layer is a tight, sharp shadow that grounds the element. The second is a medium diffusion that provides the main shadow mass. The third is a wide, soft ambient shadow. Together they create a shadow that looks like it exists in a real lighting environment.

This is exactly how design systems like Tailwind CSS and Shadcn define their shadow scales. Look at Tailwind's shadow-lg:

box-shadow:
  0 10px 15px -3px rgba(0, 0, 0, 0.1),
  0 4px 6px -4px rgba(0, 0, 0, 0.1);
Enter fullscreen mode Exit fullscreen mode

Two layers with negative spread values to pull the shadow edges in slightly.

Color matters more than you think

Using rgba(0, 0, 0, 0.2) for every shadow is the CSS equivalent of using pure black for all text. It works, but it looks harsh and disconnected from the rest of your color scheme.

Better shadows use a darkened version of the background color or the element's dominant color:

/* Generic black shadow */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);

/* Tinted shadow - blends with blue background */
box-shadow: 0 4px 12px rgba(0, 50, 100, 0.2);

/* Brand-colored shadow for accent elements */
.button-primary {
  background: #6C5CE7;
  box-shadow: 0 4px 14px rgba(108, 92, 231, 0.4);
}
Enter fullscreen mode Exit fullscreen mode

Colored shadows make elements feel like they're emitting light, which is physically accurate for lit screens. A bright blue button would cast a slightly blue shadow on a real surface. Replicating this in CSS creates a subtle but noticeable improvement in visual quality.

The inset keyword

Adding inset reverses the shadow to the inside of the element:

/* Pressed button effect */
.button:active {
  box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
}

/* Inner glow */
.card {
  box-shadow: inset 0 0 20px rgba(255, 255, 255, 0.1);
}
Enter fullscreen mode Exit fullscreen mode

Inset shadows are great for pressed states, input fields (creating a recessed appearance), and adding subtle inner highlights to glass-morphism designs.

Performance notes

Box shadows are painted during the composite step of rendering. Animating box-shadow directly causes repaints on every frame, which can be janky:

/* Avoid: repaints on every frame */
.card:hover {
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
  transition: box-shadow 0.3s;
}

/* Better: use opacity on a pseudo-element */
.card::after {
  content: '';
  position: absolute;
  inset: 0;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
  opacity: 0;
  transition: opacity 0.3s;
}

.card:hover::after {
  opacity: 1;
}
Enter fullscreen mode Exit fullscreen mode

The pseudo-element approach keeps the shadow static and only animates opacity, which is a composited property and can be GPU-accelerated.

For experimenting with shadow layers, colors, and values visually, I built a box shadow generator at zovo.one/free-tools/box-shadow-generator that lets you stack multiple shadows and copy the resulting CSS.

Good shadows are the difference between a UI that looks like a student project and one that looks professionally designed. Layer them. Tint them. Scale the offset and blur together. And above all, stay consistent with your implied light source. The details add up.


I'm Michael Lip. I build free developer tools at zovo.one. 350+ tools, all private, all free.

Top comments (0)