In this guide
What you will learn
- CSS Box Shadow Syntax, Value by Value
- Why Single Shadows Look Fake
- Building Layered Shadows
- Tinted Shadows Instead of Pure Black
Most CSS shadows look fake for the same reasons: a single hard layer, pure black at too high an opacity, and offsets that ignore where light would actually come from. This guide breaks down every value in the CSS box shadow syntax, then shows how to stack layered shadows, tint them to match your palette, turn them into an elevation scale, and keep them convincing in dark mode without hurting performance.
CSS Box Shadow Syntax, Value by Value
A single shadow takes up to six parts, in this order:
box-shadow: [inset] offset-x offset-y [blur-radius] [spread-radius] [color];
/* example */
box-shadow: 0 4px 12px 0 rgb(15 23 42 / 0.15);
- offset-x moves the shadow right (positive) or left (negative). It is usually 0, because most interfaces imply light from directly above.
- offset-y moves the shadow down (positive) or up (negative). A positive value reads as an element lifted off the page.
- blur-radius softens the edge. It cannot be negative, and 0 produces a hard edge. Larger values spread the fade over a wider area, which also makes the shadow look lighter.
- spread-radius grows (positive) or shrinks (negative) the shadow before it is blurred. A negative spread is the secret to shadows that pool under an element instead of glowing on all four sides.
- color accepts any CSS color. If you leave it out, it defaults to
currentColor, the element's text color, which is rarely what you want, so always set it.
Two behaviors are worth remembering. Shadows follow border-radius, so a rounded card automatically gets a rounded shadow. And shadows never affect layout: they take up no space and do not push neighboring elements away, although an ancestor with overflow: hidden will clip them.
Inset shadows
The inset keyword draws the shadow inside the border instead of outside, as if the surface were pressed in. Inset shadows sit above the element's background but below its content, which makes them useful for input fields, pressed button states and inner highlights:
.input {
box-shadow: inset 0 1px 2px rgb(15 23 42 / 0.12);
}
.button:active {
box-shadow: inset 0 2px 4px rgb(15 23 42 / 0.2);
}
To get a feel for each value, the CSS box shadow generator gives you sliders for horizontal and vertical offset, blur, spread and opacity, plus a shadow color field and an inset checkbox, with a live preview and a copyable box-shadow line.
Why Single Shadows Look Fake
A real object casts more than one shadow. Right next to it sits a small, dark contact shadow where almost no light reaches; further out, a larger and fainter shadow forms as light wraps around the edges. One box-shadow value can describe only one of these, so it ends up either too sharp to feel soft or too blurry to feel grounded.
The other giveaway is opacity. A shadow such as 0 10px 30px rgba(0, 0, 0, 0.5) reads as a dark smudge rather than depth. Realism comes from combining several weak shadows, not one strong one.
Building Layered Shadows
Separate shadows with commas to stack them; the first one in the list is painted on top. A reliable starting pattern is to double the offset and blur at each layer while keeping every layer's opacity low:
.card {
box-shadow:
0 1px 2px rgb(15 23 42 / 0.06),
0 2px 4px rgb(15 23 42 / 0.06),
0 4px 8px rgb(15 23 42 / 0.06),
0 8px 16px rgb(15 23 42 / 0.06),
0 16px 32px rgb(15 23 42 / 0.06);
}
Each layer adds a little density in a different band, and together they create a smooth falloff: dense and tight near the edge, wide and soft further out. Smaller offsets and blurs read as an element resting close to the page; larger ones lift it higher. A few refinements make the result more convincing:
- Keep the offset-to-blur ratio consistent. If offset-y is roughly half the blur on every layer, the light source feels coherent.
- Use negative spread on the widest layer, for example
0 24px 48px -12px, so the largest shadow sits under the element instead of haloing its sides. - Never let horizontal offsets disagree. If one layer shifts left and another shifts right, the shadows imply two light sources.
A practical workflow is to dial in one layer at a time in the generator, copy each line, and join them with commas in your stylesheet.
Tinted Shadows Instead of Pure Black
Pure black shadows desaturate whatever they fall on, which is why a black shadow on a warm cream or a blue-tinted background looks grey and dirty. Real shadows pick up the color of the surface and the surrounding light. The fix is to build the shadow from a dark, saturated version of the background hue:
:root {
--shadow-color: 220 40% 15%;
}
.card {
box-shadow:
0 1px 2px hsl(var(--shadow-color) / 0.08),
0 4px 8px hsl(var(--shadow-color) / 0.08),
0 12px 24px hsl(var(--shadow-color) / 0.1);
}
Storing hue, saturation and lightness as a space-separated custom property lets you reuse one shadow color at different alpha values. On a colored section, such as a purple hero, shift the shadow hue toward that section's color so cards inside it still look like they belong. The color shades generator is a quick way to find a deep on-hue shade to use as the shadow base, and the HEX opacity calculator turns any opacity into an 8-digit HEX or rgba() value if you prefer those formats. For more on how alpha values behave, read our guide to HEX color opacity.
Turn Shadows Into an Elevation Scale
Shadows deserve tokens just like colors and spacing. Define a short elevation scale where each step represents a height above the page, then use only those steps:
:root {
--shadow-color: 220 40% 15%;
--shadow-1: 0 1px 2px hsl(var(--shadow-color) / 0.08);
--shadow-2:
0 1px 2px hsl(var(--shadow-color) / 0.06),
0 4px 8px hsl(var(--shadow-color) / 0.08);
--shadow-3:
0 2px 4px hsl(var(--shadow-color) / 0.06),
0 8px 16px hsl(var(--shadow-color) / 0.08),
0 16px 32px -8px hsl(var(--shadow-color) / 0.12);
--shadow-4:
0 4px 8px hsl(var(--shadow-color) / 0.06),
0 16px 32px hsl(var(--shadow-color) / 0.1),
0 32px 64px -16px hsl(var(--shadow-color) / 0.16);
}
.card { box-shadow: var(--shadow-1); }
.dropdown { box-shadow: var(--shadow-3); }
.modal { box-shadow: var(--shadow-4); }
Four to six steps usually cover everything: resting cards, raised cards on hover, dropdowns and popovers, and modals. Name the steps by meaning and document them next to your color tokens; the conventions in our guide on naming colors in a design system work just as well for shadows.
One gotcha: a custom property that contains var() is resolved on the element where it is declared. If you override --shadow-color on a colored section, the --shadow-1 to --shadow-4 values inherited from :root keep the old color. Redeclare the elevation tokens in that section's rule when you need a locally tinted scale.
Performance: Animate Opacity, Not box-shadow
Static shadows are cheap enough for normal use. The cost appears when shadows change. Transitioning box-shadow on hover repaints the shadow on every frame, and large blur radii make each repaint more expensive, which can cause stutter across a grid of cards on lower-powered devices.
The common fix is to render the hover shadow on a pseudo-element and fade it in with opacity, which browsers can usually animate without repainting:
.card {
position: relative;
box-shadow: var(--shadow-1);
}
.card::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: var(--shadow-3);
opacity: 0;
transition: opacity 200ms ease;
pointer-events: none;
}
.card:hover::after {
opacity: 1;
}
Because an outer shadow is only drawn outside the border box, the transparent pseudo-element does not hide the card's content even though it sits on top. Note that filter: drop-shadow() is a different tool: it follows the alpha outline of icons and transparent images, but it has no spread or inset option.
Shadows in Dark Mode
Shadows nearly vanish on dark backgrounds because there is little brightness left to darken, and simply raising the opacity tends to produce muddy halos. Dark themes communicate elevation differently:
- Lighten the surface. Raised elements use a slightly lighter surface color than the page, so elevation reads through brightness rather than shadow.
- Use stronger shadows sparingly. Menus and modals still benefit from shadows, but they need near-black colors, higher opacity and usually fewer layers.
- Add an edge highlight. A one-pixel inset highlight defines the top edge where a shadow cannot.
@media (prefers-color-scheme: dark) {
:root {
--shadow-color: 220 50% 3%;
--shadow-1:
0 1px 2px hsl(var(--shadow-color) / 0.5),
inset 0 1px 0 rgb(255 255 255 / 0.05);
/* redefine the other steps the same way */
}
}
Because the tokens are redefined in one place, every component updates automatically. The light and dark theme previewer generates matching custom properties for both themes from an accent color, a useful starting point for the surfaces your shadows sit on, and our article on building a dark mode palette that still feels premium covers choosing those surface colors.
Conclusion
Realistic shadows come down to a few habits: understand what each value does, stack several faint layers instead of one heavy one, tint shadows with the hue of the surface, and lock the results into a small elevation scale that adapts to dark mode. Build one convincing layer at a time in the box shadow generator, combine the layers into tokens, and every card, menu and modal will share the same believable light. If you are pairing shadows with translucent panels, our glassmorphism CSS guide shows how the two work together.
FAQ
Frequently Asked Questions
How do I make a box shadow look realistic in CSS?
Stack two to five faint shadows instead of using one strong one, increasing the offset and blur with each layer. Keep every layer at a low opacity, point them all in the same direction, and tint the shadow color toward the hue of the background instead of using pure black.
Can you add multiple box shadows to one element?
Yes. Separate each shadow with a comma inside a single box-shadow declaration. The first shadow in the list is painted on top, and you can mix outer and inset shadows in the same list.
What is the difference between box-shadow and drop-shadow?
The box-shadow property always follows the rectangular box of the element, including its border-radius. The drop-shadow() filter follows the visible alpha outline instead, so it works for transparent PNGs, SVG icons and irregular shapes. Drop-shadow has no spread value and no inset option.
Why is my box shadow not showing?
The most common causes are a parent with overflow hidden that clips the shadow, a shadow color with zero alpha or the same color as the background, and offsets, blur and spread that are all zero. Also check that a later rule is not overriding the declaration with box-shadow none.