There are three ways to add a shadow in CSS: box-shadow, text-shadow and filter: drop-shadow(). The short answer: use box-shadow when the shadow should follow the element’s rectangular box, text-shadow when it should follow the glyphs, and drop-shadow() when it has to follow the shape of a cut-out image or icon.
All three are one-liners, and what separates them is the order of their values and what shape the shadow ends up being. Get that wrong and a shadow you meant to trace a transparent PNG comes out as a rectangle. Everything here is backed by the CodePen examples below and by screenshots I took on 9 October 2026 in Chrome 154, Firefox 157 and WebKit 27.2 on macOS 26.5.1.
Sponsored
Basics 1: how box-shadow works
The short answer: the shadow takes the shape of the element’s box. Give the element rounded corners and the shadow follows them.
▼The order of the values
box-shadow: [inset] offset-x offset-y [blur] [spread] [color];
▼What each value does
| Value | Optional? | Meaning |
|---|---|---|
inset |
Yes | Casts the shadow inside the element. Goes first if used |
| offset-x | No | Distance to the right. Negative moves it left |
| offset-y | No | Distance down. Negative moves it up |
| blur | Yes | Larger is softer. 0 gives a hard edge |
| spread | Yes | Grows or shrinks the shadow itself. Negative shrinks it |
| color | Yes | Falls back to the color property if omitted |
Only the two offsets are required. Give just those and you get a hard-edged shadow with no blur at all.
.shadow-box {
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.3);
}
A hard shadow on an image
See the Pen
box-shadow by りん@エンジニア (@rinblog0408)
on CodePen.
For a crisp shadow behind an image or a block, set the blur to 0 and give it a definite colour.
.shadow {
box-shadow: 10px 10px 0 #c3a400;
}
An inner shadow (inset)
The short answer: put inset at the front and the shadow falls inside the element. That is how you make something look recessed.
/* drop a shadow in from the top edge so the box looks sunken */
.inner-shadow {
box-shadow: inset 0 6px 10px rgba(0, 0, 0, 0.5);
}
For a recessed input, layering a 1px ring on top of the inner shadow sharpens the outline.
.input-well {
background: #eef2f7;
box-shadow:
inset 0 2px 4px rgba(0, 0, 0, 0.25),
inset 0 0 0 1px #c8d2de;
}
inset belongs to box-shadow alone. Neither text-shadow nor drop-shadow() has an inner mode. To hollow out text you need a different technique entirely, usually one that clips a background.
Inner shadows show up constantly when you build your own form controls — I used them throughout styling checkboxes and radio buttons with CSS.
A shadow on the bottom edge only
The short answer: push the shadow down with offset-y, then pull it back in with a negative spread so the sides disappear.
/* a soft shadow under the element and nowhere else */
.shadow-bottom {
box-shadow: 0 10px 10px -8px rgba(0, 0, 0, 0.6);
}
Reading the numbers: move down 10px, blur by 10px, shrink the whole shadow by 8px. The shrinking is what removes the parts that were poking out at the sides and the top, leaving only the bottom.
For the top edge instead, make offset-y negative.
.shadow-top {
box-shadow: 0 -10px 10px -8px rgba(0, 0, 0, 0.6);
}
For an even halo on all four sides, set both offsets to zero.
.shadow-all {
box-shadow: 0 0 18px rgba(0, 0, 0, 0.45);
}
Three stacked shadows look more natural than one
The short answer: layer three shadows at different distances and opacities and the result reads better than any single one.
.card {
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.16),
0 4px 8px rgba(0, 0, 0, 0.14),
0 12px 24px rgba(0, 0, 0, 0.12);
}
Close and dark, medium, far and faint. Real shadows change their softness with distance, so three layers sit more comfortably than one flat shade. This is the treatment for anything meant to float: cards, modals, menus.
Using it instead of a border
The short answer: zero blur and spread only gives you a line. Unlike border, it does not change the element’s size.
/* a 1px line plus a soft ring outside it, for focus states */
.ring {
box-shadow:
0 0 0 1px #9fb4cc,
0 0 0 5px rgba(74, 122, 181, 0.35);
}
Adding a border grows the element and nudges everything around it; a box-shadow is only painted, so nothing shifts. That makes it the right tool for a ring that appears on focus and disappears again.
Basics 2: how text-shadow works
The short answer: it shadows the glyphs themselves, and unlike box-shadow it supports neither inset nor spread.
▼The order of the values
text-shadow: offset-x offset-y [blur] [color];
.shadow-text {
text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.3);
}
Offset shadows on headings
See the Pen
shadow-text by りん@エンジニア (@rinblog0408)
on CodePen.
The offset shadow you see on headings everywhere is also one line. Set the blur to 0 and pick a colour that stands on its own.
.shadow {
text-shadow: 4px 4px 0 #c3a400;
}
Outlining text
The short answer: stack eight unblurred shadows and you get an outline. It is how you keep white text legible over a photograph.
.outlined {
color: #fff;
text-shadow:
2px 0 0 #000, -2px 0 0 #000,
0 2px 0 #000, 0 -2px 0 #000,
1.5px 1.5px 0 #000, -1.5px 1.5px 0 #000,
1.5px -1.5px 0 #000, -1.5px -1.5px 0 #000;
}
Four directions alone leaves gaps on the diagonals. Add the four corners as well and the outline looks an even thickness all the way round.
-webkit-text-stroke expresses the same thing in two lines.
.outlined-stroke {
color: #fff;
-webkit-text-stroke: 2px #000;
paint-order: stroke fill; /* paint the stroke under the glyph so it stays full width */
}
Without paint-order: stroke fill, half the stroke eats into the letterform. I counted the white pixels in the same word both ways: 2,411 without it, 8,972 with it. The letters lose more than two thirds of their visible weight when it is missing.
For an outline and nothing else, this is the version worth maintaining — fewer values to keep in sync. It rendered identically in Chrome 154, Firefox 157 and WebKit 27.2.
Making text glow
The short answer: zero offsets, blur only, two of them stacked, and the text looks lit.
.glow {
color: #fff;
text-shadow:
0 0 10px #ffe66b,
0 0 20px #ffb300;
}
Put the brighter colour on the inside and the deeper one outside it. The effect only reads on a dark background, so it is not a general-purpose trick.
Sponsored
Applying several shadows with commas
You can layer as many shadows as you like, separated by commas. The first one listed is painted on top.
.shadow-box {
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.3), -5px -5px 5px rgba(0, 0, 0, 0.3);
}
text-shadow behaves the same way. Splitting a light shadow to the top left and a dark one to the bottom right to fake depth is an application of this, and it is the basis of nearly every surface in neumorphism in UI design.
You can also mix the shadow colour on the spot with color-mix(). All three browsers painted it without complaint.
.mixed {
box-shadow: 0 8px 12px color-mix(in oklab, #000 45%, transparent);
}
That saves writing out an rgba() every time you want a translucent black. The rest of the modern colour functions are in the CSS color() function, and when to use oklch() instead.
What is the difference between blur and spread?
Blur softens the shadow’s edge. Spread changes the size of the shadow itself. They sit next to each other as the third and fourth values and do completely different things.
/* base: blur only */
.shadow-box {
box-shadow: 0 5px 5px rgba(0, 0, 0, 0.3);
}
/* more blur: the edge softens and spreads out */
.shadow-box.blur {
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
}
/* add spread: the shadow itself gets bigger */
.shadow-box.spread {
box-shadow: 0 5px 5px 8px rgba(0, 0, 0, 0.3);
}
/* negative spread: the shadow contracts */
.shadow-box.shrink {
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.3);
}
Negative spread is the one that earns its keep in real work. Push the blur up and pull the spread negative and the shadow gathers underneath the element, which is what reads as floating.
Sponsored
drop-shadow follows the shape, not the box
See the Pen
filter:drop-shadow by りん@エンジニア (@rinblog0408)
on CodePen.
The short answer: for a transparent PNG icon, or an element cut out with clip-path, use filter: drop-shadow(). Where box-shadow casts from the element’s rectangle, drop-shadow() follows the opaque pixels — the alpha channel.
.shadow {
filter: drop-shadow(10px 10px 0 #c3a400);
}
I put two copies of the same star-shaped transparent PNG side by side. drop-shadow() produced a star-shaped shadow; box-shadow produced a rectangle. That single difference is behind most of the “why does my shadow look wrong” cases.
Three things to watch with drop-shadow
One. There is no inset and no spread. You get two offsets, a blur radius and a colour, and that is the whole vocabulary.
Two. The blur radius is interpreted differently. The third value of drop-shadow() is the standard deviation of a Gaussian blur, so the same number spreads much further. I gave an identical square the same values in both properties and measured how far the shadow reached before it faded out.
▼How far the shadow reaches for the same value (measured in Chrome 154)
| Value given | box-shadow |
drop-shadow() |
|---|---|---|
| 5px | 4px | 9px |
| 10px | 9px | 19px |
| 20px | 19px | 37px |
| 40px | 38px | 67px |
Roughly double, right across the range. Copy a box-shadow value straight into drop-shadow() and you get twice the blur you designed. When you port one to the other, start at about half.
Three. Multiples are written differently. You chain them with spaces — drop-shadow(...) drop-shadow(...) — not with commas.
/* glow first, then drop a shadow underneath */
.icon {
filter: drop-shadow(0 0 6px #e8551f) drop-shadow(0 6px 4px rgba(0, 0, 0, 0.4));
}
clip-path on the same element removes the shadow
The short answer: put clip-path and filter: drop-shadow() on the same element and the shadow is clipped away with everything else. Move the filter to the parent.
/* no shadow appears: it gets clipped off */
.triangle {
background: #4a7ab5;
clip-path: polygon(50% 0, 100% 100%, 0 100%);
filter: drop-shadow(8px 8px 3px rgba(0, 0, 0, 0.6));
}
<div class="triangle-wrap">
<div class="triangle"></div>
</div>
/* the shadow appears when the filter lives on the parent */
.triangle-wrap {
filter: drop-shadow(8px 8px 3px rgba(0, 0, 0, 0.6));
}
.triangle {
background: #4a7ab5;
clip-path: polygon(50% 0, 100% 100%, 0 100%);
}
The order is: the filter draws the shadow, then clip-path cuts the element down — so on one element the shadow falls outside the clip and is thrown away. Chrome, Firefox and WebKit all behaved identically here.
Combine it with box-shadow and the shadow gets duplicated too
The short answer: on the same element, drop-shadow() treats “the element plus its box-shadow” as one silhouette and copies the lot.
.double {
background: #4a7ab5;
box-shadow: 26px 26px 0 #cc4444; /* red shadow */
filter: drop-shadow(-30px 60px 0 #2aa84a); /* green shadow */
}
Render that and the green shadow is not one square. It is a stepped shape made of the element and its red shadow together, because filter operates on whatever the element finished painting.
If you did not plan for it, it looks like a bug. When you want both, put the box-shadow on an inner element and the drop-shadow() on an outer one.
A flow for choosing between them
The short answer: three questions in order, and you are done.
- 1. Is the thing casting the shadow text? → yes:
text-shadow - 2. Is a shadow shaped like the element’s box (square or rounded) fine? → yes:
box-shadow - 3. Does it have to follow a cut-out shape? → yes:
filter: drop-shadow()
Two and three are where people hesitate. Anything with a transparent background (a PNG or an SVG), and anything reshaped by clip-path or by something other than border-radius: 50%, is case three. Everything else is case two.
There is a second axis worth knowing: if stacking order matters, stay with box-shadow. An element with a filter creates a new stacking context, which means its children can never rise above elements outside it, no matter how large their z-index is.
/* adding this filter pushes .child behind .sibling */
.parent {
position: absolute;
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.4));
}
.parent .child {
position: absolute;
z-index: 9999; /* still cannot come forward */
}
.sibling {
position: absolute; /* an element written after .parent */
}
Remove the filter from the same markup and .child jumps back in front of .sibling. This is why a dropdown or tooltip suddenly starts disappearing behind its neighbours after someone adds a shadow to its container.
Shadow generators are fine to use, by the way — what comes out is the same box-shadow value. There are only ever four numbers to read (right, down, blur, spread) plus the alpha of the colour, so check the output against the table above before you paste it in.
The three compared
Ask whether the thing you are shadowing is a box, some text, or a cut-out shape, and the choice makes itself.
▼box-shadow, text-shadow and drop-shadow()
| Item | box-shadow |
text-shadow |
drop-shadow() |
|---|---|---|---|
| Shadow shape | The element’s box | The glyphs | The opaque pixels |
| Blur | Yes | Yes | Yes, about twice as wide for the same value |
| Spread | Yes | No | No |
inset (inner shadow) |
Yes | No | No |
| Multiple shadows | Comma separated | Comma separated | Space separated |
| Typical use | Cards, buttons, inputs | Headings, logo text | Transparent PNGs, icons, cut-outs |
In practice, cards and buttons take box-shadow, headline copy takes text-shadow, and logos or illustrations take drop-shadow(). That split covers nearly everything. Frameworks ship shadow utilities of their own as well, so whether you write these by hand is partly a question of what the rest of your CSS looks like — Bootstrap and Tailwind compared goes into that choice.
Shadows are nearly invisible on dark backgrounds
The short answer: a black shadow on a dark surface does nothing. To lift an element in dark mode, use a 1px light line instead of, or alongside, the shadow.
/* give the edge definition on a dark surface */
.card-dark {
background: #2a3340;
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.14),
0 8px 16px rgba(0, 0, 0, 0.6);
}
I rendered the same card light and dark side by side: against the dark background the black shadow was barely distinguishable from the surface behind it. Carry your light-mode shadow values straight into dark mode and the element stops looking raised at all. When you switch palettes, revisit the shadows in the same pass.
Summary
- Text takes
text-shadow, boxes takebox-shadow, cut-out shapes takedrop-shadow() - Inner shadows (
inset) and spread exist only onbox-shadow - For a bottom-only shadow, push it down and pull the spread negative
drop-shadow()blurs about twice as far asbox-shadowfor the same numberclip-pathon the same element clips the shadow away — move thefilterto the parent- On dark backgrounds, add a 1px light line because the shadow will not read
▼Looking for books on HTML, CSS and JavaScript?