What are CSS shadow effects, and which property draws which shadow?
The properties differ mainly in what shape they trace. box-shadow follows the border box, curves from border-radius included. text-shadow follows glyph outlines and any text decoration. drop-shadow() works from the alpha channel of whatever got rendered, and feDropShadow is the SVG filter primitive that does the same job inside a filter element.
None of them touch layout. A box-shadow does not change box model dimensions, so a wide shadow overlaps its neighbors unless the margin leaves room (MDN, 2026).
Spec-wise, box-shadow lives in CSS Backgrounds and Borders Module Level 3 and text-shadow in CSS Text Decoration. Filter Effects Module Level 1 covers drop-shadow() and feDropShadow.
Support is a non-issue now. As of October 2026, MDN lists box-shadow and text-shadow as widely available since July 2015, and drop-shadow() since September 2016.
How the box-shadow value syntax works
A box-shadow takes between two and four lengths, plus an optional color and an optional inset keyword. The order is offset-x, offset-y, blur radius, spread radius, and only the first two are required (MDN, 2026).
- Start with offset-x and offset-y. Negative values push the shadow left and up.
- Blur radius comes next. It defaults to 0, which gives a sharp edge, and it can’t be negative.
- Spread only matters when the shadow has to grow or shrink around the box, so skip it otherwise.
- Pick a color, ideally rgba with a low alpha. Leave it out and the shadow falls back to the color property.
- Put inset in front for an inner shadow. Extra shadows go after a comma.
Tuning four numbers by eye gets old fast, so a box-shadow generator with a live preview saves time on the first pass. MDN also ships its own interactive generator.
Offsets and blur radius
offset-x moves the shadow sideways and offset-y moves it up or down. Set both to 0 and the shadow sits directly behind the element, so you only see it if blur or spread is set.
Bigger blur values make the shadow larger and lighter. The specification doesn’t define an exact blur algorithm (MDN, 2026), which means a shadow that carries a design should get checked in more than one engine.
Rounded corners are handled for you. The shadow copies the element’s border-radius, so a circle gets a circular shadow.
Spread radius and negative spread
Spread resizes the shadow independently of blur. Positive values grow it, negative values shrink it, and 0 keeps it the size of the element.
Zero offsets, zero blur and a positive spread give you a solid ring, which works as a second border. Go negative with a downward offset instead, say 0 10px 6px -6px, and the shadow stays on the bottom edge.
Spread is the least used value, and the one that makes one-edge shadows possible from a single declaration.
Inset shadows
An inset shadow is clipped to the padding box and painted above the background but below the content (MDN, 2026). It reads as content pressed into the box.
Mixing inset and regular shadows breaks transitions. If one shadow in a pair has inset and the other does not, the whole list becomes uninterpolable and the change snaps with no animation.
Keep inset consistent across both states, and give the resting state a transparent inset shadow if the hover state needs one.
text-shadow for glow, outlines and readable text
Two offsets are required, blur radius is optional, and so is color. The shadow follows glyph outlines and any text decoration.
Unlike box-shadow, it is inherited (MDN, 2026), so one rule on a container styles every heading and paragraph inside it. It also applies to ::first-line and ::first-letter.
Color can sit before or after the lengths. If you leave it out the browser picks one, so define it every time. Blur defaults to 0 and can’t be negative.
There is a spec wrinkle here. MDN’s prose documents only offsets, blur and color, but its formal syntax, taken from the newer Level 4 drafts, also lists a spread length and an inset keyword, and MDN warns that browsers may not implement every part.
Stick to the three-length form in production. And as with box-shadow, the first shadow in a list paints on top.
For a glow, set the offsets to 0, add blur and stack a few layers. MDN’s example pairs 1px 1px 2px black with two blue glows (0 0 1em and 0 0 0.2em) on white text.
Outlines get faked with four shadows, one per direction at 1px with no blur and the same color.
A long shadow needs one shadow per pixel step, each offset one pixel further than the last, so a 20px tail costs 20 comma-separated values.
Layering glow values by hand takes trial and error, which is where a text shadow generator earns its place.
When to use drop-shadow() instead of box-shadow
Reach for drop-shadow() when the shadow has to follow visible pixels, as with transparent PNGs and clipped shapes. SVGs too. box-shadow is the better fit for rectangles and rounded boxes that need inset, spread or a plain list of layers.
| Tool | Shadow follows | Accepted values | Best for |
|---|
| box-shadow | Border box and border-radius | Offsets, blur, spread, color, inset | Cards, buttons, inputs |
| text-shadow | Glyph outlines and decoration | Offsets, blur, color | Headings, glow, outlines |
| drop-shadow() | Alpha channel of the rendered element | Offsets, blur, color | Transparent PNG, SVG, clipped shapes |
MDN describes the drop shadow as a blurred, offset copy of the alpha mask composited below the image, whereas box-shadow paints behind the entire box (MDN, 2026).
Inset and spread aren’t allowed in drop-shadow(). Its third value is the standard deviation of a Gaussian blur, and it can’t be negative. There’s no comma list either, so chain several drop-shadow() functions inside one filter value, separated by spaces.
A W3C issue opened in October 2020 (fxtf-drafts #409) asks for a spread value in drop-shadow(). MDN’s page still lists spread as not allowed, and Can I use notes that no browser supports it, so a spread you rely on in box-shadow has no equivalent here.
Identical numbers do not give identical shadows. MDN defines drop-shadow()’s third value as a standard deviation, while box-shadow’s blur radius is the distance over which the color fades, and CSS-IRL found box-shadow renders darker and heavier with the same values. Adjust by eye when you swap one for the other.
A clip-path cuts away everything outside the shape, shadow included. Apply the drop-shadow() filter to the clipped element’s parent instead (CSS-IRL).
Graphics built as SVG can carry the shadow in the file itself. Inside a filter element, feDropShadow does the job of one drop-shadow() call, with dx and dy (both default to 2), stdDeviation, flood-color and flood-opacity.
It is shorthand for a feGaussianBlur, feOffset, feFlood, feComposite and feMerge chain, and it works in linearRGB by default (MDN, 2026). Set color-interpolation-filters to sRGB when the shadow color looks off.
How to layer shadows for realistic depth and elevation
Several comma-separated box-shadows, with offset and blur growing step by step and all pointing away from the same light, fall off far more softly than one blurred box.
Tobias Ahlin published the pattern in 2019: five layers at 1, 2, 4, 8 and 16px, each with a matching blur and the same low alpha (0.12 in his example).
- Pick one light direction and keep the x:y offset ratio fixed for every shadow on the page. Josh Comeau uses 1:2.
- Take the shadow color from the background hue, with lower saturation and lightness, and store it in a custom property.
- Write layers where offset and blur double each time, such as 1px 2px 2px, 2px 4px 4px and 4px 8px 8px.
- Split the alpha across the layers. Every added layer needs a lower alpha to keep the total strength.
- Compare the stack with the single-layer original on the real background, then drop a layer if the page feels heavy.
A distant sun casts every shadow at the same angle, which is why one ratio works for a whole page (Josh Comeau, updated April 2026).
As an element rises, offset and blur grow while opacity drops. Bigger shadows also pull the eye first, so a dialog should sit visibly above the header.
Color matters more than people expect. Transparent black over a colored page desaturates it. Matching the hue, as in hsl(220deg 60% 50%) on an hsl(220deg 100% 80%) background, keeps the surface colorful.
A single box-shadow blurs a box silhouette, which is why one layer looks flat. Comeau also warns that each layer is more work for the device: five layers cost roughly five times one, though in his own testing the difference was not significant.
Older inexpensive phones are where it shows, and animating a layered stack is a bad idea on any hardware.
Tailwind CSS ships a ready scale from shadow-2xs to shadow-2xl. Its sm through xl presets are two-layer stacks at 0.1 alpha, while shadow-2xl is one layer at 0.25 (Tailwind docs, v4.3).
Bootstrap 5.3 stays with one layer per preset: 0 .125rem .25rem at .075, 0 .5rem 1rem at .15 and 0 1rem 3rem at .175. Component shadows remain off until the Sass flag $enable-shadows is set.
Treat any of these scales as tokens. In a design system, one elevation scale plus a –shadow-color variable per background means a theme change edits one value, not every component.
Material Design 3 defines elevation as the relative distance between two surfaces along the z-axis, which is the right mental model for that scale.
Dark mode needs its own token set. Black shadows barely register on near-black surfaces, so raise the surface lightness for higher layers and let the shadow play a supporting role.
How to animate shadows without dropping frames
Animate the opacity of a pseudo-element that carries the final shadow, not the box-shadow values themselves. A direct box-shadow transition repaints the element on every frame.
web.dev’s rendering guidance states that changing any property other than transform or opacity always triggers paint, and that paint is often the most expensive part of the pixel pipeline. A desktop absorbs that cost. A phone or a busy layout shows it as stutter (Tobias Ahlin).
The fix is mechanical. Put the larger hover shadow on ::after, start it at opacity 0, and transition opacity instead. Cloud 66’s Brent Graham (updated 2022) reached the same conclusion.
The pseudo-element must match the box size and border-radius and sit behind the content. It is more CSS than the one-line version, and Ahlin says as much.
A direct box-shadow transition is one declaration and easy to read. The cost is a repaint on every frame, and that grows on phones and complex layouts.
The pseudo-element approach changes only opacity, and it ranked smoothest in a public CodePen test (robjoeol). You do end up writing the shadow twice, and the stylesheet gets longer.
Animating drop-shadow() took the least painting and rendering time in that same CodePen test, and it follows the alpha shape. Safari is the catch. It does not apply a filter to child elements while they animate (WebKit bug 219729, per Can I use), and Comeau saw input lag in Safari on a filtered element containing a text field.
The sources agree that drop-shadow() costs less paint than a direct box-shadow transition. They stop short of crowning it: the CodePen test names the pseudo-element the best overall choice, and Safari behavior is the catch.
Check the result instead of guessing. In Chrome, open the Rendering tab and turn on Paint flashing, which flashes the screen green wherever repainting happens (Chrome for Developers). A full-screen flash during a hover means the transition repaints more than the card.
Card hover lifts are where this matters most, and CSS card hover effects are the usual home for the pattern.
A hover shadow is decorative motion, so it can switch off entirely. Inside a prefers-reduced-motion: reduce block, set the transition to none. MDN describes the feature as detecting a request to minimize non-essential motion, available across browsers since January 2020.
Shadow contrast and accessibility
A shadow can lift text off a busy background, but WCAG measures text against its background, so a shadow never replaces a passing color pair. Use it as backup, not as the primary fix.
WCAG 2.2 sets the bar at 4.5:1 for normal text at Level AA, and 3:1 for large text, meaning 18 point, or 14 point bold (about 24px and 18.5px). Level AAA (SC 1.4.6) asks for 7:1. There is no rounding, so 4.499:1 does not meet 4.5:1 (W3C WCAG 2.2, Understanding SC 1.4.3).
Start from a pair that passes on its own, using the basics of color contrast. An accessible color palette generator hands you passing pairs before any shadow is added.
W3C’s definition counts a wide halo that fills in a letter’s inner details as background, not as part of the letter. Applied to a thick glow, that means measuring the letters against the glow color at its heaviest.
The W3C ACT rule for minimum contrast checks the highest possible contrast of each character, and its own text-shadow example reports a range, not one ratio.
Shadows cut both ways. Acquia’s dev guide flags #999999 text with a white 2px 2px 4px shadow as a failure because the shadow lowers effective contrast, and its suggested fix is the opposite: a dark rgba(0, 0, 0, 0.8) shadow at the same offsets.
In forced colors mode, such as Windows High Contrast, both box-shadow and text-shadow are forced to none (MDN, 2026). Restore lost boundaries with a border, and reserve forced-color-adjust: none for elements that must keep authored colors.
Tailwind’s ring utilities are solid box-shadows, so a focus ring built only from one vanishes in forced colors. Keep an outline underneath. Tailwind’s own docs point to outline-hidden, which hides the default outline but keeps it in forced colors mode.
And never let a shadow carry state alone. Pair a hover or selected shadow with a color, border or text change.
Where CSS shadows break, and which browsers support them
As of October 2026, current browsers draw box-shadow, text-shadow and drop-shadow(), so when a shadow breaks the cause is usually structural. An ancestor clips it, an engine has a bug, or a legacy table is involved. Prefix lines copied from old tutorials add nothing.
Can I use data (2026) puts the box-shadow prefix in Chrome 4 to 9, Firefox 3.5 and 3.6, and Safari 3.1 to 5. Unprefixed support starts at Chrome 10, Firefox 4 and Safari 5.1.
The filter prefix lived longer. -webkit-filter covered Chrome 18 to 52 and Safari 6 to 9, and unprefixed support starts at Chrome 53, Firefox 35 and Safari 9.1.
Testing in Chrome alone hides engine differences. A short cross-browser compatibility pass on Safari and Firefox catches the rest.
Ancestor clipping is the common one. Overflow hidden or clip on a parent cuts the shadow at the parent’s edge. Edge and IE up to 11 suppress box-shadow in tables with border-collapse: collapse, and legacy Edge skips a filter when the element or its parent has a negative z-index.
For the clipping case, padding on the clipping parent usually works. So does overflow: visible, and overflow: clip with overflow-clip-margin is the other route. A merged pull request in simstudioai/sim (#4670) used the first one, adding 3px of left padding so Tailwind’s truncate utility stopped cutting a 3px highlight shadow.
An element’s own overflow affects only its content, never its own shadow. In November 2025 the Ladybird browser fixed a bug where it clipped a box-shadow when overflow: hidden sat on the same element, because overflow only affects the element’s content (Ladybird commit, 2025).
CSS Shadow Effects FAQ
How do you add a shadow to only one side of an element?
Combine a negative spread with an offset toward the target side. A value like 0 10px 6px -6px shrinks the shadow on every side, then pushes it down so only the bottom edge shows.
Swap the offsets to target the left, right or top edge.
How do you remove an existing shadow?
Set the property to none: box-shadow: none or text-shadow: none. Both default to none, according to MDN (2026).
A drop-shadow() sits inside the filter property, so filter: none removes it together with any other filter on that element.
Can text-shadow be animated?
Yes. MDN lists text-shadow as animatable, with color, offsets and blur interpolating as a shadow list. A shorter list is padded with transparent shadows.
Each frame still triggers paint, so keep text-shadow transitions short and rare.
Does box-shadow work on inline elements like a span?
Yes, because box-shadow applies to all elements. A span that wraps across lines is sliced at each break by default.
Add box-decoration-break: clone and every line fragment gets its own full shadow (MDN).
How do you create a neumorphism effect, and is it accessible?
Neumorphism stacks two box-shadows on a surface that matches the page: a light one toward the top left and a dark one toward the bottom right. Both flip to inset for a pressed state.
Accessibility is its weak point, because the look depends on low contrast. Controls need a visible border or fill, and CSS neumorphism recipes rarely add one.
Building a Reusable Shadow System
CSS shadow effects stay consistent across a site when the work follows a fixed order, starting with the failure state and ending with motion.
Start with a border or outline fallback for forced colors. Then set three or four elevation tokens, with one shadow color per background. Pseudo-element opacity handles every hover lift after that.
The order follows dependency. Forced colors removes shadows entirely, so a component needs a boundary that survives before any token is worth tuning, and animation touches only the tokens that already exist.
The price is range. A closed set of three or four levels rules out one-off shadows, and each animated component carries its shadow twice.
One condition would reorder the list. Can I use data, read in October 2026, still records a Safari bug (WebKit 219729) where a filter skips child elements while they animate, and a fix would put drop-shadow() animation beside the pseudo-element as a default.