I spent about forty minutes once trying to match a shadow from a Figma file. The designer had given me a colour, an X and Y offset, a blur, and a spread, which is everything you need, and I still could not get it to look right. The shadow in the browser was heavier and flatter than the mockup, and I kept nudging the blur radius up and down like that would fix it.
It would not, because the mockup had two shadows stacked. A tight dark one for contact, a wide soft one for ambient light. Figma showed them as separate layers in a panel I had collapsed. Once I wrote them as a comma-separated pair the match was instant.
That is the thing about the CSS properties in this guide. None of them are complicated. All of them have one or two behaviours that are not obvious from the syntax, and you can lose an afternoon to any of them. This guide is those behaviours, written down.
Every generator linked here runs in your browser, updates a live preview as you drag, and hands you the CSS. I build them at toolz.dev and use them constantly, mostly because reading a four-value shorthand is harder than looking at a rectangle.
TL;DR: Use Box Shadow for depth, Text Shadow for glow and outlines, Border Radius for corners including the elliptical ones, CSS Filters for blur, brightness and colour effects, Glassmorphism for frosted panels, Neumorphism for soft raised surfaces, Gradients for backgrounds, and the CSS Unit Converter when you need px in rem. All of them run client-side with a live preview. The two rules worth carrying: real shadows are layered, and
backdrop-filterneeds something behind it to blur.
Which generator do I need?
| You want | Property | Tool |
|---|---|---|
| A card that lifts off the page | box-shadow |
Box Shadow |
| Glowing, outlined or embossed text | text-shadow |
Text Shadow |
| Rounded or pill-shaped corners | border-radius |
Border Radius |
| Blur, dim, desaturate or tint an element | filter |
CSS Filter |
| A frosted panel over a photo | backdrop-filter |
Glassmorphism |
| A soft button that looks pressed into the surface | layered box-shadow |
Neumorphism |
| A colour transition background | linear-gradient |
Gradient |
| To turn 24px into rem | none | Unit Converter |
The rest of this guide is what each one does and where it bites.
How does box-shadow work?
The shorthand is five values and a colour:
box-shadow: 0 4px 12px 0 rgb(0 0 0 / 0.15);
/* │ │ │ │ └ colour */
/* │ │ │ └ spread: grow or shrink */
/* │ │ └ blur: 0 is a hard edge */
/* │ └ vertical offset */
/* └ horizontal offset */
Offsets move the shadow. Blur softens its edge, spreading it outward by roughly half the blur value in each direction. Spread grows or shrinks the shadow before the blur is applied, and a negative spread is the trick for a shadow that sits tucked under an element rather than haloing it.
The part people miss: shadows stack. A comma-separated list renders back to front, first shadow on top:
box-shadow:
0 1px 2px rgb(0 0 0 / 0.20), /* contact: tight, darker */
0 8px 24px rgb(0 0 0 / 0.12); /* ambient: wide, lighter */
That pair is why a designer's shadow looks better than yours. One shadow reads as a sticker. Two reads as an object with weight. The Box Shadow Generator lets you add layers and drag each one separately, which is the only sane way to tune them.
inset moves the shadow inside the border box, which is how you get an input that looks recessed, or the inner half of a neumorphic surface.
One performance note that has bitten me on real projects: animating box-shadow forces a repaint every frame. If you want a card to lift on hover, animate transform and cross-fade a second element that holds the bigger shadow. The MDN box-shadow reference documents the syntax in full.
What is text-shadow good for besides drop shadows?
Same idea, one value fewer. text-shadow has no spread:
text-shadow: 1px 1px 2px rgb(0 0 0 / 0.5);
Its interesting uses are not shadows at all.
Outlines. Four hard shadows at zero blur, one per diagonal, fake a stroke around text. It is heavier than -webkit-text-stroke but it works everywhere and it renders outside the glyph rather than splitting the difference:
text-shadow: 1px 1px 0 #000, -1px 1px 0 #000,
1px -1px 0 #000, -1px -1px 0 #000;
Glow. Zero offset, large blur, saturated colour, repeated two or three times to build intensity.
Legibility over images. A single soft dark shadow under white text buys you contrast when the background is a photo you do not control. It is not a substitute for a proper scrim, and it will not save a failing contrast ratio, but it stops the descenders disappearing into a bright patch.
Build all three on the Text Shadow Generator. If you are chasing a contrast requirement rather than an effect, check the result against WCAG with the Color Contrast Checker instead of eyeballing it.
Why does border-radius take eight values?
Most people use one. The full form is two groups of four, separated by a slash:
border-radius: 20px 40px 60px 80px / 40px 20px 80px 60px;
/* ─────horizontal──── ──────vertical───── */
Before the slash: the horizontal radius of each corner, clockwise from top-left. After it: the vertical radius. Give both and each corner becomes an ellipse rather than a quarter circle. That is how you get organic blob shapes without an SVG.
Two behaviours worth knowing.
Percentages resolve against different axes. A horizontal radius in % is a percentage of the element's width, the vertical one of its height. So border-radius: 50% on a rectangle gives an ellipse, not a rounded rectangle, which surprises people the first time.
The browser scales radii down when they overlap. If two adjacent radii add up to more than the side they share, all radii are reduced proportionally until they fit. You cannot overflow the shape, which is why border-radius: 9999px reliably produces a pill.
The Border Radius Generator exposes all eight values with per-corner sliders and emits the shortest valid shorthand, which is worth having because writing the slash form by hand is genuinely unpleasant.
What can CSS filters do, and what do they cost?
filter applies graphics operations to an element and everything inside it:
filter: blur(4px) brightness(1.1) saturate(1.2);
Functions compose left to right, and order changes the result. Blurring then brightening is not the same as brightening then blurring.
| Function | Does | Watch for |
|---|---|---|
blur() |
Gaussian blur | Expensive at large radii; blurs edges outward |
brightness() |
Multiplies lightness | Above 1 clips highlights to white |
contrast() |
Pushes values from mid grey | Crushes detail fast |
grayscale() |
Removes saturation | Cheaper than a duplicate asset |
sepia() |
Warm tone map | Combine with hue-rotate() for other tints |
hue-rotate() |
Shifts hue by an angle | Does not preserve perceived lightness |
drop-shadow() |
Shadow that follows alpha | Unlike box-shadow, follows the actual shape |
That last row is the one worth remembering. box-shadow shadows the border box, so a transparent PNG of a logo gets a rectangular shadow. filter: drop-shadow() traces the alpha channel and shadows the logo itself.
The cost is real. Filters push an element onto its own compositing layer, and blur() in particular is expensive over a large area. Fine on a button. Think twice on a full-page overlay on a low-end phone. MDN's filter reference lists every function and its interpolation behaviour.
Try combinations on the CSS Filter Generator, which shows the preview and the exact declaration side by side.
How do I build a glassmorphism panel that works?
Frosted glass is one property doing the work:
background: rgb(255 255 255 / 0.15);
backdrop-filter: blur(12px) saturate(180%);
border: 1px solid rgb(255 255 255 / 0.25);
backdrop-filter filters what is behind the element rather than the element itself. Three things follow from that, and they account for nearly every "why does my glass look like a grey box" question.
It needs something behind it. Over a flat background colour, a blurred backdrop is that same flat colour. The effect only exists over a photo, a gradient, or overlapping content.
The background must be translucent. A fully opaque background hides the blurred backdrop completely. Somewhere between 10% and 25% white is the usual range.
Saturation is what sells it. Real glass concentrates colour. saturate(180%) alongside the blur is the difference between frosted glass and a smudge.
Add the hairline border because glass has an edge, and check contrast before shipping: text over a blurred photo passes or fails depending on the photo, and a panel that reads perfectly on your hero image may be unreadable on the next one.
The Glassmorphism Generator previews over an actual image, which matters here more than for any other effect in this guide.
Is neumorphism worth using?
Neumorphism, or soft UI, makes elements look extruded from the background rather than floating above it. Two shadows, opposite corners, one lighter than the surface and one darker:
background: #e0e5ec;
box-shadow: 8px 8px 16px #a3b1c6,
-8px -8px 16px #ffffff;
Swap them for a pressed state, or use inset on both for a carved-in look.
The technique has one hard constraint: the element and its background must be the same colour. The effect is entirely the two shadows, so a neumorphic button on a different background stops reading as extruded and starts reading as broken.
It also has an accessibility problem worth being honest about. The whole aesthetic is low contrast by construction, and a button whose only affordance is a soft shadow can fail WCAG's non-text contrast requirement for interactive components. If you use it, give buttons a real border or a focus style that does not depend on the shadow.
The Neumorphism Generator derives both shadow colours from your surface colour, which is the fiddly part to do by hand.
When do I need a unit converter?
More often than you would think, because design tools speak px and good CSS often does not.
rem is relative to the root font size, so a layout in rem scales when a user changes their browser's default text size. em is relative to the element's font size, which makes it useful for padding that should track its own text and treacherous when it compounds through nested elements.
The arithmetic is trivial. Doing it forty times while translating a spec is where mistakes creep in, especially if the root size is not 16px. The CSS Unit Converter takes a configurable root and shows every unit at once.
For gradients, the Gradient Generator covers linear, radial and conic, and there is a longer write-up in the CSS gradient guide.
Putting them together: a card, start to finish
The properties are rarely used alone. Here is a card that uses four of them, built the way I would build it.
Start with the shape and the resting shadow:
.card {
border-radius: 12px;
background: #fff;
box-shadow:
0 1px 2px rgb(0 0 0 / 0.06),
0 4px 12px rgb(0 0 0 / 0.08);
transition: transform 150ms ease, box-shadow 150ms ease;
}
Two shadows again, both light. A resting card should barely be lifted. Save the drama for the hover state.
.card:hover {
transform: translateY(-2px);
box-shadow:
0 2px 4px rgb(0 0 0 / 0.08),
0 12px 28px rgb(0 0 0 / 0.12);
}
The translateY does most of the perceived work. Movement reads as lift more convincingly than a bigger shadow does, and it is cheap because transforms are composited rather than repainted.
If the card sits on a photo instead of a flat page, swap the white background for glass:
.card {
background: rgb(255 255 255 / 0.15);
backdrop-filter: blur(12px) saturate(180%);
border: 1px solid rgb(255 255 255 / 0.25);
}
Note what changed and what did not. The radius and the shadows carry over untouched. Only the surface treatment differs, which is the useful mental model: shape and depth are one decision, surface is another.
What about browser support and fallbacks?
Most of this is old and safe. box-shadow, text-shadow, border-radius and filter have been supported everywhere for years, and you can write them without a second thought.
backdrop-filter is the exception worth handling. Support is broad now, but it is the one property here that produces a genuinely broken-looking result when it is missing: a panel with a 15% white background and no blur is a pale smear over a photo. Guard it:
.glass {
background: rgb(255 255 255 / 0.85); /* readable fallback */
}
@supports (backdrop-filter: blur(1px)) {
.glass {
background: rgb(255 255 255 / 0.15);
backdrop-filter: blur(12px) saturate(180%);
}
}
The fallback is a mostly opaque panel, which is not the effect but is perfectly usable. The enhanced version only applies where the browser can deliver it. MDN's @supports reference covers the syntax, and it is worth reaching for any time a property has a visually broken failure mode rather than a merely plainer one.
How do I keep effects consistent across a project?
The failure mode on a real codebase is not one bad shadow. It is fourteen slightly different shadows, because everyone who needed a card wrote their own.
Define them once as custom properties and use the names:
:root {
--shadow-sm: 0 1px 2px rgb(0 0 0 / 0.06);
--shadow-md: 0 1px 2px rgb(0 0 0 / 0.06),
0 4px 12px rgb(0 0 0 / 0.08);
--shadow-lg: 0 2px 4px rgb(0 0 0 / 0.08),
0 12px 28px rgb(0 0 0 / 0.12);
--radius: 12px;
}
.card { border-radius: var(--radius); box-shadow: var(--shadow-md); }
.card:hover { box-shadow: var(--shadow-lg); }
Three shadow steps is usually enough. If you find yourself adding a fourth, it is often because a component needs a different elevation rather than a different shadow, and the answer is to reuse an existing step.
This also makes dark mode tractable. Shadows that work on white are close to invisible on a dark surface, where depth usually comes from a lighter border or a subtle glow instead. Redefine the tokens inside a media query and every component follows:
@media (prefers-color-scheme: dark) {
:root {
--shadow-md: 0 1px 2px rgb(0 0 0 / 0.4);
--shadow-lg: 0 8px 24px rgb(0 0 0 / 0.5);
}
}
Generate each value once with the tools above, paste it into the token block, and stop thinking about it. That is the whole point of having a generator: not to write CSS you could not write by hand, but to stop spending attention on values that only need deciding once.
Where these effects cause accessibility problems
Every property in this guide manipulates contrast, and contrast is the thing accessibility requirements are mostly about. Four specific traps.
Interactive components need visible boundaries. WCAG 2.2 requires 3:1 contrast for the parts of a control that identify it. A button whose only edge is a soft shadow can fail that, which is the structural problem with neumorphism and the reason a border or a solid focus ring is not optional there.
Glass panels inherit their background's contrast problem. Text on a frosted panel is legible or not depending on the photo behind it, and the photo often changes. Test against your worst image, not your hero. Raising the panel's background opacity is usually a better fix than making the text bolder.
Filters change contrast silently. brightness() and contrast() applied to a container also apply to the text inside it. A filter: brightness(1.15) on a card to make it pop can quietly push your body text under the 4.5:1 threshold. Check afterwards with the Color Contrast Checker, not before.
Do not let a shadow be the only state indicator. If hover, focus and pressed differ only by shadow depth, keyboard users and anyone with low vision lose the distinction. Pair the shadow change with a transform, an outline, or a colour shift.
None of this makes the effects unusable. It makes them things to verify rather than assume, which takes about a minute per component with a contrast checker open.
Do these tools send my work anywhere?
No. Every generator here runs entirely in the browser. There is no upload step and no server round trip, which is why they respond instantly as you drag a slider. That is the same reason the rest of the catalogue works offline, covered in the data privacy guide.
Common mistakes
- One shadow instead of two. The single most visible difference between a hand-written shadow and a designed one.
- Animating
box-shadowon hover. Repaints every frame. Animatetransformand cross-fade instead. backdrop-filterover a solid colour. Nothing to blur, so nothing happens.- Opaque background on a glass panel. Hides the effect you just wrote.
- Neumorphism on a mismatched background. The illusion depends on them being identical.
border-radius: 50%expecting a rounded rectangle. On a non-square element you get an ellipse.- Shipping any of these without a contrast check. Shadows and glass both reduce contrast, and both are easy to over-tune on a good monitor.



