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);
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);
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);
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);
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);
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);
}
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);
}
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;
}
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)