CSS box-shadow examples from basic cards to layered effects

Quick answer

💡CSS box-shadow takes the form: offset-x offset-y blur-radius spread-radius color, with an optional inset keyword at the start. Multiple shadows are comma-separated, painted from last to first. Shadows do not affect layout — they paint outside the border-box. Parent overflow:hidden clips shadows that extend beyond the element's box. Use filter:drop-shadow() for non-rectangular elements like PNGs with transparency.

Error symptoms

  • Shadow does not appear despite valid CSS
  • Shadow is clipped by a parent container
  • Shadow looks correct on desktop but janky on mobile during scroll
  • Inset shadow not visible on element with no background
  • Shadow disappears in dark mode
  • filter:drop-shadow does not follow the shape of a PNG image

Common causes

  • Parent element has overflow:hidden which clips painted content
  • Element has no background-color — inset shadows are invisible without one
  • Large blur-radius animated on mobile triggers expensive paint
  • Dark theme color is too similar to the shadow color in dark mode
  • box-shadow used on non-rectangular PNG instead of filter:drop-shadow
  • Cascade override from a higher-specificity rule sets box-shadow to none

When it happens

  • Card components and content panels
  • Modal dialogs and popovers
  • Button hover and focus states
  • Design system elevation tokens
  • Sticky headers and bottom navigation bars

Box shadow syntax: offset, blur, spread, and color

The CSS box-shadow property accepts a comma-separated list of shadow definitions, each of which follows a specific value order. A fully specified shadow takes the form: inset? offset-x offset-y blur-radius spread-radius color. All of these components are optional except offset-x and offset-y, but their order is fixed and cannot be rearranged.

Offset-x and offset-y control how far the shadow is displaced from the element. Positive offset-x moves the shadow right; negative moves it left. Positive offset-y moves it down; negative moves it up. A shadow with both offsets at zero is centered beneath the element, which creates a symmetrical ambient glow effect when combined with a blur radius. These two values are the only required ones — box-shadow: 0 4px black is a valid minimal declaration.

Blur-radius controls the softness of the shadow edge. A blur of 0 produces a hard-edged shadow that exactly mirrors the element's shape. Larger values spread the shadow outward and fade the edges. There is no upper practical limit, but very large values (above 40-50px) become computationally expensive on mobile because the browser must rasterize a larger area. The blur algorithm in CSS box-shadow is defined as a Gaussian blur, though browser implementations may approximate it for performance.

Spread-radius expands or contracts the shadow before the blur is applied. A positive spread of 4px makes the shadow 4px larger on all sides than the element — useful for creating a border-like outline effect (box-shadow: 0 0 0 3px royalblue produces a focus ring). A negative spread contracts the shadow inward, which is useful for inset shadow effects that create the impression of a recessed surface rather than a floating one. Spread defaults to 0 when omitted.

Color accepts any valid CSS color value: named colors, hex codes, rgb(), rgba(), hsl(), hsla(), and the newer oklch() and color() functions. For realistic shadows, semi-transparent colors using rgba() or hsla() produce better results than opaque colors because they interact correctly with colored backgrounds. The conventional wisdom is that shadows should use a dark, semi-transparent color (rgba(0,0,0,0.15)) rather than a hard-coded gray, so the shadow adapts to its background. The color value can also be currentColor, which inherits the text color of the element — occasionally useful for colored shadows in icon components.

Example 1

A valid shadow can vanish when any ancestor has overflow:hidden without sufficient padding.

Card shadow clipped by parent overflow:hidden

❌ Wrong

<div class="panel">
  <article class="card">
    <h2>Monthly report</h2>
    <p>Updated today</p>
  </article>
</div>
<style>
.panel { overflow: hidden; padding: 0; background: #f5f5f5; }
.card { background: white; border-radius: 8px;
        box-shadow: 0 8px 24px rgba(0,0,0,.18); }
</style>

✅ Fixed

<div class="panel">
  <div class="shadow-room">
    <article class="card">
      <h2>Monthly report</h2>
      <p>Updated today</p>
    </article>
  </div>
</div>
<style>
.panel { overflow: hidden; background: #f5f5f5; }
.shadow-room { padding: 24px; }
.card { background: white; border-radius: 8px;
        box-shadow: 0 8px 24px rgba(0,0,0,.18); }
</style>

box-shadow paints outside the border-box of the element but does not reserve layout space for itself. When a parent has overflow:hidden, the browser clips any paint that extends beyond the parent's border-box, which includes the shadow. Adding a wrapper with padding inside the clipping container gives the shadow enough room to render. The panel can still clip content — only the shadow wrapper needs the extra space.

Inset shadows and inner glow effects

Adding the inset keyword at the beginning of a shadow definition changes the shadow from a drop shadow (painted outside the element) to an inset shadow (painted inside the element's border-box). Inset shadows create the visual impression of a recessed surface, a pressed button state, or an inner border, and they respond differently to the offset-x and offset-y values compared to outset shadows.

For an inset shadow, positive offset-y pushes the shadow toward the top of the element's interior — which creates a darkened top edge that suggests the element is a container sitting below the level of the page. Negative offset-y pushes the shadow to the bottom interior edge. Positive offset-x pushes the shadow to the left interior wall, and negative pushes it to the right. This is the opposite intuition from drop shadows, which can be confusing at first. A common inset shadow pattern for an input field in a pressed or focus state is: box-shadow: inset 0 2px 4px rgba(0,0,0,0.15).

Inset shadows are only visible on elements that have a non-transparent background. An element with background: transparent will show inset shadows — but they will be painted on top of the transparent area, which means they will only be visible if the parent's background contrasts with the shadow color. For a solid background, the contrast is guaranteed. For a gradient or image background, the shadow still paints correctly because it occupies the same stacking context as the element's background.

You can combine inset and outset shadows on the same element by including both in the comma-separated list. The property does not have an inset-only or outset-only mode — each shadow in the list has its own optional inset keyword. A common design pattern is to use a small outset shadow for drop elevation and a small inset shadow for a subtle inner edge highlight: box-shadow: 0 4px 12px rgba(0,0,0,.12), inset 0 1px 0 rgba(255,255,255,.15). This creates a card that appears to float above the surface while also having a subtle glass-like top edge.

Negative spread on an inset shadow is particularly useful. box-shadow: inset 0 0 0 1px rgba(0,0,0,.1) draws a 1px inner border without affecting layout, and the negative spread on an inset shadow like box-shadow: inset 0 0 12px -4px rgba(0,0,0,.2) draws a soft inner glow that fades before reaching the center of the element. These effects are commonly used for form input focus states and for creating the impression of depth in icon containers.

Layering multiple shadows on a single element

CSS box-shadow accepts a comma-separated list of shadow definitions and renders them in back-to-front order: the last shadow in the list is painted first (furthest from the viewer), and the first shadow is painted on top (closest to the viewer). This layering model allows you to simulate the complex light behavior of real physical objects, where ambient light produces a wide soft shadow and a directional key light produces a smaller, sharper shadow.

The most common layering pattern in design systems uses two shadows of different scales. A close shadow represents the light from an overhead source: 0 2px 4px rgba(0,0,0,.12). A far shadow represents ambient scatter: 0 12px 32px rgba(0,0,0,.08). Together, they produce a more naturalistic depth than a single shadow with a medium blur. This is the approach used in Material Design's elevation system, where each elevation level (dp2, dp4, dp8, dp16) maps to a specific pair of box-shadow values.

Three-layer shadows add a third element: an extremely small tight shadow that represents contact shadow — the darkest point directly beneath the element where it is closest to the surface. The three-layer pattern: 0 1px 1px rgba(0,0,0,.12), 0 2px 4px rgba(0,0,0,.1), 0 8px 24px rgba(0,0,0,.08). The three opacity values decrease as the shadow spreads further, which mimics how light diffuses over distance. This pattern is used in high-fidelity design systems like Linear, Notion, and Stripe's component library.

Layering also enables creative effects that a single shadow cannot achieve. A colored outer glow: 0 0 0 4px rgba(66,153,225,.5), 0 4px 12px rgba(0,0,0,.15) creates a focus ring combined with a drop shadow, useful for accessible interactive elements. A four-directional border simulation: box-shadow: 0 0 0 2px currentColor creates an outline that follows the element's border-radius without affecting layout. A ridge highlight and base shadow combined: 0 -1px 0 rgba(255,255,255,.1), 0 2px 4px rgba(0,0,0,.2) adds both a top highlight and a bottom shadow in one declaration. However, be aware that performance costs scale with the number of shadows — on mobile, more than three or four shadows with large blur values can degrade scroll performance significantly.

Two moderate shadows look more realistic than one large one

A close shadow (0 2px 4px rgba(0,0,0,.12)) plus an ambient shadow (0 8px 24px rgba(0,0,0,.08)) produces a naturalistic depth effect that matches how real objects cast shadows under typical indoor lighting. A single shadow with a 60px blur radius looks unnatural and is expensive to paint on mobile.

Example 2

Two moderate shadows create naturalistic depth; animating transform instead of box-shadow avoids paint work.

Layered button shadow with performant hover

❌ Wrong

<button class="cta">Generate report</button>
<style>
.cta {
  border: 0;
  background: #1a1a2e;
  color: white;
  padding: 12px 24px;
  border-radius: 6px;
  box-shadow: 0 0 80px rgba(0,0,0,.85);
  transition: box-shadow 200ms;
  cursor: pointer;
}
.cta:hover {
  box-shadow: 0 0 140px rgba(0,0,0,.95);
}
</style>

✅ Fixed

<button class="cta">Generate report</button>
<style>
.cta {
  border: 0;
  background: #1a1a2e;
  color: white;
  padding: 12px 24px;
  border-radius: 6px;
  box-shadow:
    0 2px 4px rgba(0,0,0,.12),
    0 8px 20px rgba(0,0,0,.14);
  transition: transform 150ms ease, box-shadow 150ms ease;
  cursor: pointer;
}
.cta:hover {
  transform: translateY(-2px);
  box-shadow:
    0 4px 8px rgba(0,0,0,.14),
    0 12px 28px rgba(0,0,0,.18);
}
</style>

A very large blur radius (80-140px) forces the browser to paint a huge area on every frame during the hover transition, which causes jank on mobile. Moderate layered shadows (two shadows with small-to-medium blur values) produce a more realistic depth effect at a fraction of the paint cost. Animating transform:translateY() moves the element on the compositor thread without triggering layout or paint, which keeps hover interactions smooth.

box-shadow vs filter: drop-shadow — which to use and when

CSS provides two distinct ways to add shadows to elements: the box-shadow property and the filter:drop-shadow() function. They produce visually similar results for simple rectangular elements but behave completely differently for non-rectangular content, and they have different performance characteristics.

box-shadow always follows the rectangular border-box of the element, regardless of what is rendered inside it. If you apply box-shadow to an img element displaying a PNG with transparent pixels, the shadow will form a rectangle around the entire image dimensions — it will not follow the shape of the non-transparent content. This is almost never the desired behavior for cutout images, icons, or SVG graphics with transparency.

filter:drop-shadow(offset-x offset-y blur-radius color) calculates the shadow based on the alpha channel of the rendered content, not the border-box. Applied to a PNG with a transparent background, drop-shadow follows the exact contour of the visible pixels. Applied to a CSS-shaped element (one using clip-path, border-radius, or complex shapes), drop-shadow follows the clipped outline. This is the correct tool for adding shadows to icons, product photos with removed backgrounds, and SVG illustrations.

The syntax differs from box-shadow: filter:drop-shadow() uses spaces (not commas) between values and does not support the spread-radius parameter or the inset keyword. There is also no multi-shadow syntax — you cannot comma-separate multiple drop-shadow values directly in the filter property, though you can chain multiple filter functions: filter:drop-shadow(0 2px 4px rgba(0,0,0,.12)) drop-shadow(0 8px 16px rgba(0,0,0,.08)).

Performance differences are real and hardware-dependent. box-shadow is a composited property in most modern browsers, meaning the shadow can be painted on the GPU compositor layer separately from the page content. filter:drop-shadow() requires rasterizing the element's content to compute the alpha mask before applying the blur, which happens on the CPU in most browsers and cannot be composited as efficiently. For animated shadows on frequently-updated elements, box-shadow is generally faster. For static shadows on non-rectangular elements, drop-shadow produces the correct shape at an acceptable performance cost.

Shadow performance and compositor layer promotion

CSS shadows can be among the most expensive visual effects in terms of rendering performance, particularly on mobile devices. Understanding the browser's rendering pipeline helps you make informed decisions about shadow complexity and animation strategies.

The browser rendering pipeline has three relevant stages for shadows: layout (calculating the size and position of elements), paint (rasterizing elements into pixels), and composite (combining painted layers and sending them to the GPU). box-shadow does not participate in layout — it does not affect the element's size or the positions of its siblings. But it does participate in paint, and a large blur radius means the browser must rasterize a larger area of pixels. A box-shadow with a 60px blur radius extends 60px in all directions from the element's border-box, requiring the browser to rasterize a rectangle that is 120px wider and taller than the element itself.

Animating box-shadow between values forces the browser to repaint the shadow on every frame of the animation, even if only the blur radius or color is changing. Repaint is expensive and happens on the CPU. For a 60Hz display, that means 60 repaint operations per second, each covering the full shadow area. On a mid-range Android device, this can cause frame drops and jank that makes the animation feel choppy.

The recommended strategy for animated shadows is to avoid animating the shadow directly and instead animate transform:translateY() or opacity to convey elevation changes. translateY() and opacity are compositor-only properties — they run entirely on the GPU without triggering repaint. The visual result of a slight upward movement combined with a transition to a slightly lighter shadow (which the browser only needs to paint once for the hover state) approximates the elevation change at a fraction of the rendering cost.

For elements that need box-shadow and are also individually animated or scrolled, consider promoting them to their own compositor layer using will-change:box-shadow or transform:translateZ(0). This hints to the browser that the element should be rasterized independently, which can prevent it from invalidating surrounding content during repaint. However, each compositor layer consumes GPU memory, so this optimization should be used selectively — not applied globally to all shadowed elements. The DevTools Layers panel (available in Chrome and Firefox) shows which elements have their own compositor layers and how much memory each consumes.

Never animate box-shadow blur-radius on mobile

Animating from a small to a large blur radius triggers expensive repaint on every frame. Instead, define the start and end shadow states statically and animate transform:translateY(-2px) for the elevation lift. The shadow change happens instantaneously at hover start and end, while the movement is smooth and GPU-composited.

Dark mode shadows with CSS custom properties

Shadows that look elegant on light backgrounds often become invisible or look wrong on dark backgrounds. A rgba(0,0,0,0.15) shadow on a white card is clearly visible, but on a dark card against a dark page background, the dark shadow blends into the dark background and the elevation effect disappears. Dark mode shadow design requires a fundamentally different approach, not just the same shadow value.

The most common technique for dark mode shadows is to shift the shadow hue toward the hue of the background color rather than using a pure black shadow. On a dark navy background (#1a1a2e), a shadow using rgba(0,0,0,0.5) is still too dark to create visible separation. A more effective approach is to use the background color itself at full opacity as the shadow color: box-shadow: 0 4px 12px #0d0d1a. This creates a shadow that is darker than the card surface and lighter than if you were using black, producing a subtle but real elevation effect.

An alternative approach that works well in design systems is to use CSS custom properties to define shadow values that change with the color scheme. Define token variables at the theme level and reference them in component CSS. When the prefers-color-scheme media query (or a .dark class on the root element) switches the theme, the custom property values update and all components inherit the correct shadow for the current mode.

A concrete pattern using custom properties: at the :root level, define --shadow-elevation-1: 0 2px 4px rgba(0,0,0,.12); for light mode, and inside a @media (prefers-color-scheme: dark) block or a [data-theme=dark] selector, redefine it to --shadow-elevation-1: 0 2px 8px rgba(0,0,0,.5), 0 0 0 1px rgba(255,255,255,.04);. The second shadow in the dark mode definition adds a very subtle white border — rgba(255,255,255,.04) — which creates a visible edge between the card and the dark background without resorting to a hard border. This technique, popularized by Josh W. Comeau's writing on shadows, is widely used in dark-mode-capable design systems.

A third approach for dark mode is to rely on lightness contrast rather than shadow darkness. Instead of a dark shadow, use a slightly lighter background on the elevated element compared to the page background. Combined with a very subtle border, this creates the visual impression of elevation without a visible shadow at all. This is the approach used by GitHub's dark mode component design, where cards appear elevated primarily through background color contrast rather than shadow values. The box-shadow in this case acts as a thin border: box-shadow: 0 0 0 1px rgba(255,255,255,.08).

CSS box-shadow checklist

  • Verify no ancestor has overflow:hidden without sufficient padding for the shadow.
  • Check DevTools Computed panel that box-shadow is not overridden by a higher-specificity rule.
  • For PNG images or SVG with transparency, use filter:drop-shadow() not box-shadow.
  • Use two or three moderate shadows instead of one huge blur for naturalistic depth.
  • Avoid animating large blur-radius values — animate transform or opacity instead.
  • In dark mode, use a slightly lighter semi-transparent color instead of pure rgba(0,0,0,x).
  • Define shadow values as CSS custom properties that update with prefers-color-scheme.
  • Test scroll and hover animations on a mid-range Android device, not only desktop.

Frequently asked questions

Why is my box-shadow not showing?

The most common causes are: a parent element has overflow:hidden without enough padding, a higher-specificity rule sets box-shadow:none, the element has no background-color (required for inset shadows), or the shadow color is too similar to the background. Open DevTools, inspect the element's Computed styles, and check whether box-shadow is applied and whether any ancestors clip the painted area.

How do I add multiple shadows in CSS?

Separate each shadow definition with a comma: box-shadow: 0 2px 4px rgba(0,0,0,.12), 0 8px 24px rgba(0,0,0,.08). Shadows are painted in back-to-front order — the last shadow in the list is rendered furthest from the viewer. There is no practical limit on the number of shadows, but more than three or four with large blur values can degrade performance on mobile.

What is the difference between box-shadow and filter:drop-shadow?

box-shadow always follows the rectangular border-box of the element, ignoring transparency in images. filter:drop-shadow() follows the alpha channel of the rendered content — the visible pixel outline. Use box-shadow for solid-background UI elements like cards and buttons. Use filter:drop-shadow() for PNG images with transparent backgrounds, SVG icons, or elements shaped by clip-path.

Does box-shadow affect page layout?

No. box-shadow is painted outside the border-box but does not participate in layout — it does not push siblings, add scroll width, or change the element's size. However, because it paints outside the box, a parent with overflow:hidden will clip the shadow. Add padding to the parent or use a wrapper element to give the shadow room to render.

How do I make a box-shadow follow a border-radius?

It does this automatically. box-shadow always follows the computed border-radius of the element — no additional CSS is required. A circular element (border-radius:50%) will have a circular shadow. A pill-shaped element with large border-radius will have a matching pill-shaped shadow. This is one of the advantages of box-shadow over border-based outline techniques.

How do I create a focus ring with box-shadow?

Use a zero-offset shadow with zero blur and a positive spread: box-shadow: 0 0 0 3px rgba(66,153,225,.6). This creates a visible ring around the element that follows its border-radius without affecting layout. Combine it with outline:none (cautiously — only when box-shadow provides equivalent visibility) for a custom focus indicator that passes WCAG 2.1 focus visibility requirements.

Why does box-shadow animation cause jank on mobile?

Animating box-shadow triggers repaint on every frame because the browser must re-rasterize the shadow area. Large blur values require rasterizing a proportionally larger area. On mobile CPUs, this can cause frame drops. The fix is to animate transform:translateY() and opacity instead, which run on the GPU compositor without triggering repaint. Change the shadow value only on hover start and end, not throughout the transition.

How do I handle box-shadow in dark mode?

Pure black shadows (rgba(0,0,0,x)) become invisible on dark backgrounds where there is little contrast difference. Use CSS custom properties to define different shadow values per theme: a low-opacity shadow for light mode and a higher-opacity or slightly colored shadow for dark mode. An additional inset shadow of rgba(255,255,255,.05) can provide a subtle top-edge highlight that reinforces elevation in dark themes.

What does spread-radius do in box-shadow?

Spread-radius expands (positive value) or contracts (negative value) the shadow before blur is applied. A spread of 4px makes the shadow 4px larger on all sides. A spread of -4px makes it 4px smaller. With both offsets and blur at zero, a positive spread creates a solid border-like outline: box-shadow: 0 0 0 3px currentColor. A negative spread on an inset shadow draws a soft inner glow that fades before reaching the element's center.

Related guides

All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-07.