A single box-shadow: 0 4px 12px rgba(0,0,0,0.3) looks like a gray smudge glued under a card. Real objects do not cast shadows that way, and your eye knows it. The fix is not a better blur value. The fix is more than one shadow.
This guide explains the mechanism, gives you copy-paste values, and ends with a glassmorphism card you can verify in DevTools. You can write all of it by hand, with no generator.
Why one box-shadow looks flat
A real light source produces a tight, dark shadow where the object nearly touches the surface, plus a wide, faint shadow that fades into the distance. Those are two different falloffs overlapping. A single box-shadow has one blur radius, so it draws one smooth falloff. It cannot be tight and wide at the same time.
Pick a small blur and the shadow looks hard and sticker-like. Pick a large blur and the card looks like it floats in fog with no contact point. Raising opacity to compensate only darkens the smudge.
Stacking shadows solves this because the browser composites each comma-separated layer on top of the next. Short, darker layers define the edge. Long, lighter layers build the ambient glow. Together they approximate a natural falloff that no single blur can produce.
Step 1: Stack five layers with doubling offset and blur
Use this rule of thumb: each layer doubles the previous layer's vertical offset and blur, and each layer is a little less opaque.
Start with a 1px offset and 1px blur. The next layers use 2px, 4px, 8px, and 16px. Opacity falls across the stack, for example 0.10, 0.09, 0.08, 0.06, 0.04. Four to six layers works well, and five keeps the declaration readable.
Two details matter. First, keep the horizontal offset at 0 so the light appears to come from straight above. Second, the first shadow in the list paints on top, so put the tight layer first to keep the contact edge crisp.
The doubling pattern and the opacity numbers are a design heuristic, not a CSS rule. Treat them as starting values and adjust by eye on your own backgrounds.
Step 2: Pick values for small, medium, and large elevations
Elevation is the same recipe scaled up. Buttons sit low, cards sit in the middle, and modals float high. Each column below is five layers, written as y-offset and blur, then alpha. Every layer uses an x-offset of 0 and a black base color.
| Layer | Small (buttons) | Medium (cards) | Large (modals) | | --- | --- | --- | --- | | 1 | 1px 1px / 0.10 | 2px 2px / 0.10 | 4px 4px / 0.08 | | 2 | 2px 2px / 0.09 | 4px 4px / 0.09 | 8px 8px / 0.07 | | 3 | 4px 4px / 0.08 | 8px 8px / 0.08 | 16px 16px / 0.06 | | 4 | 8px 8px / 0.06 | 16px 16px / 0.06 | 32px 32px / 0.05 | | 5 | 16px 16px / 0.04 | 32px 32px / 0.04 | 64px 64px / 0.04 |
These values are illustrative starting points, not measured standards. On dark backgrounds, shadows nearly vanish. Raise the alpha, or lean on a border or a lighter surface color instead.
Step 3: Wrap the values in custom properties
Paste the table into variables once, then reuse the tokens everywhere. This is the reusable snippet:
:root {
--shadow-color: 0 0 0;
--shadow-sm:
0 1px 1px rgb(var(--shadow-color) / 0.10),
0 2px 2px rgb(var(--shadow-color) / 0.09),
0 4px 4px rgb(var(--shadow-color) / 0.08),
0 8px 8px rgb(var(--shadow-color) / 0.06),
0 16px 16px rgb(var(--shadow-color) / 0.04); We cover related ground in [how to move from quickstart to production (checklist)](/how-to-move-from-quickstart-to-production-checklist).
--shadow-md:
0 2px 2px rgb(var(--shadow-color) / 0.10),
0 4px 4px rgb(var(--shadow-color) / 0.09),
0 8px 8px rgb(var(--shadow-color) / 0.08),
0 16px 16px rgb(var(--shadow-color) / 0.06),
0 32px 32px rgb(var(--shadow-color) / 0.04);
--shadow-lg:
0 4px 4px rgb(var(--shadow-color) / 0.08),
0 8px 8px rgb(var(--shadow-color) / 0.07),
0 16px 16px rgb(var(--shadow-color) / 0.06),
0 32px 32px rgb(var(--shadow-color) / 0.05),
0 64px 64px rgb(var(--shadow-color) / 0.04);
}
.button { box-shadow: var(--shadow-sm); }
.card { box-shadow: var(--shadow-md); }
.modal { box-shadow: var(--shadow-lg); }
One gotcha: var(--shadow-color) resolves where the token is defined. If you override --shadow-color on a single card, the --shadow-md it inherits from :root will not change. Redefine the whole token on that element, or define the tokens on a shared parent.
Step 4: Add a glassmorphism card on top
Glassmorphism needs three ingredients: a translucent background, backdrop-filter to blur whatever sits behind the element, and a subtle light border. The layered shadow still works here, because the browser does not paint outer box shadows inside the element's own box. The shadow therefore does not muddy the glass.
Complete minimal example (unexecuted)
This example has no dependencies. It is unexecuted: nobody has run it in a browser, so check the output yourself with the steps below. This pairs well with s3 gateway endpoint vs interface endpoint: which to pick in depth.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Layered shadows and glass</title>
<style>
:root {
--shadow-color: 0 0 0;
--shadow-md:
0 2px 2px rgb(var(--shadow-color) / 0.10),
0 4px 4px rgb(var(--shadow-color) / 0.09),
0 8px 8px rgb(var(--shadow-color) / 0.08),
0 16px 16px rgb(var(--shadow-color) / 0.06),
0 32px 32px rgb(var(--shadow-color) / 0.04);
--shadow-lg:
0 4px 4px rgb(var(--shadow-color) / 0.08),
0 8px 8px rgb(var(--shadow-color) / 0.07),
0 16px 16px rgb(var(--shadow-color) / 0.06),
0 32px 32px rgb(var(--shadow-color) / 0.05),
0 64px 64px rgb(var(--shadow-color) / 0.04);
}
body {
margin: 0; padding: 48px 24px; display: grid; gap: 48px;
justify-items: center; background: #eef0f4; color: #1b1f2a;
font-family: system-ui, sans-serif;
}
.card {
width: 280px; padding: 24px; border-radius: 16px;
background: #fff; box-shadow: var(--shadow-md);
}
.scene {
position: relative; padding: 64px; border-radius: 24px; overflow: hidden;
background: linear-gradient(135deg, #4f46e5, #ec4899);
}
.scene::before {
content: ""; position: absolute; top: 24px; left: 60px;
width: 140px; height: 140px; border-radius: 50%; background: #fde047;
}
.glass {
position: relative; color: #fff;
background: rgb(255 255 255 / 0.18);
border: 1px solid rgb(255 255 255 / 0.35);
-webkit-backdrop-filter: blur(14px) saturate(140%);
backdrop-filter: blur(14px) saturate(140%);
box-shadow: var(--shadow-lg);
}
</style>
</head>
<body>
<article class="card"><h2>Layered shadow</h2><p>Five stacked layers.</p></article>
<div class="scene">
<article class="card glass"><h2>Glass card</h2><p>Blurred backdrop.</p></article>
</div>
</body>
</html>
Expected visual result
The white card should sit on the light gray page with a soft, even shadow: a faint dark edge hugging the bottom and a wide haze beneath it, with no visible band where one layer ends. In the gradient scene, the glass card should look like frosted glass. The yellow circle behind it should appear as a blurry glow through the card's left portion, while the circle's sharp edge stays visible outside the card.
Browser support notes
The layered box-shadow and rgb(r g b / alpha) syntax work in current evergreen browsers. For backdrop-filter, older Safari versions needed the -webkit- prefix, which is why the example declares both. Support details change, so check a current compatibility table such as caniuse.com for the browsers you must support. Always provide a readable fallback: a translucent background that stays legible even if the blur never applies.
Step 5: Verify both effects in DevTools
Do not trust your eyes alone. Open the page, right-click the white card, and choose Inspect. Then work through this checklist.
- In the Styles pane, find
box-shadow: var(--shadow-md). Hover over the variable name, or open the Computed tab and search for "box-shadow". You should see five comma-separated layers matching the values in your table. - Untick the
box-shadowdeclaration, then tick it again. The card should flip between flat and elevated. Many browsers also offer a shadow editor swatch beside the value for live tweaking. - Select the glass card and find
backdrop-filter. Untick it, and the yellow circle should sharpen behind the card. Tick it again and the blur returns. - If a declaration shows a strikethrough or warning icon, the browser rejected the value. Look for a typo, a missing unit, or a stray semicolon.
When backdrop-filter shows nothing
The typical symptom: the card looks like plain translucent white, with no blur. Toggling the declaration in step 3 above makes no visible difference. Work through these causes in order.
Also read: see google ads app campaign for indie games: avoid junk installs
- Nothing is behind the element to blur. Blurring a flat color gives the same flat color. Put a gradient, image, or shape underneath, as the example does.
- The background is opaque. A fully opaque
backgroundhides everything the filter would blur. Use an alpha value, such asrgb(255 255 255 / 0.18). - The blur is too small or zero. Try
blur(14px)to confirm the effect, then tune down. - The declaration is invalid. Check the Styles pane for a struck-through line. Also confirm you wrote
backdrop-filter: plainfilterblurs the card's own content, not what sits behind it. - An ancestor limits what the filter can see. A parent with its own
filter,opacitybelow 1, a mask, or anotherbackdrop-filtercan change what counts as the backdrop. Remove those from the ancestors temporarily and retest. This is an inference from how stacking and compositing generally behave, so confirm it in your own DevTools. - The browser needs the prefix. In older Safari, add
-webkit-backdrop-filter.
Turning it into a Tailwind utility
Because the shadows live in CSS variables, Tailwind can use them without rewriting values. An arbitrary property works in any setup that supports arbitrary values:
<div class="[box-shadow:var(--shadow-md)] rounded-2xl bg-white p-6">...</div>
For a named utility, register the value under theme.extend.boxShadow in your Tailwind config, for example soft: 'var(--shadow-md)', then use shadow-soft. Newer Tailwind releases move theme configuration into CSS. Check the documentation for your installed version rather than copying config shapes from older tutorials.
Optional shortcut: generators
If tweaking five layers by hand feels slow, a generator can speed up experimentation. One example is CSSForge, a free, client-side tool built by the author of a dev.to post. According to that post, it previews shadows on dark, light, gradient, and image backgrounds, and exports pure CSS, CSS variables, or Tailwind CSS. The post also says it needs no registration and includes a glassmorphism generator. The tool lives at cssforge-cyan.vercel.app.
Those claims come only from the author's own post and have not been independently tested. Even with a generator, the DevTools checks above still apply, because only the browser tells you what actually renders.
Where to go next
Once the tokens are in place, the next gains come from tuning rather than new syntax. Tint --shadow-color toward your background hue. Add a hover state that moves a card from medium to large elevation. Test the glass card over real photography, since the background changes how strong a shadow reads.



