CSS Shadow Effects: 16 Examples of Depth Done Right

CSS shadow effects come down to three tools: box-shadow for boxes, text-shadow for glyphs and filter: drop-shadow() for transparent shapes. One catch: forced colors mode, such as Windows High Contrast, sets box-shadow and text-shadow to none.

Below are 16 CSS shadow effects you can try and copy, followed by a guide to syntax, layering, animation and accessibility.

By Bogdan Sandu ยท Updated October 28, 2026

Elevation and realistic depth

Shadows that make a surface look like it sits above the page. Start with a token scale, then see why layered and tinted shadows read as light rather than as grey smudges.

Five-level elevation scale

  • custom properties
  • layered box-shadow
  • design tokens
Raised--sh-elev-1Chips, inputs
Card--sh-elev-2Cards, tiles
Menu--sh-elev-3Dropdowns, popovers
Toast--sh-elev-4Toasts, sticky bars
Dialog--sh-elev-5Modals, sheets

Each level is a custom property that adds one more layer, with offset and blur doubling at every step and the alpha dropping slightly. One shadow color variable feeds all five, so a theme or a tinted section changes a single value instead of every component.

View code
.sh-elev {
  --sh-elev-color: 222deg 32% 30%;
  --sh-elev-1: 0 1px 1px hsl(var(--sh-elev-color) / .12), 0 2px 2px hsl(var(--sh-elev-color) / .12);
  --sh-elev-2: 0 1px 1px hsl(var(--sh-elev-color) / .11), 0 2px 2px hsl(var(--sh-elev-color) / .11), 0 4px 4px hsl(var(--sh-elev-color) / .11);
  --sh-elev-3: 0 1px 1px hsl(var(--sh-elev-color) / .1), 0 2px 2px hsl(var(--sh-elev-color) / .1), 0 4px 4px hsl(var(--sh-elev-color) / .1), 0 8px 8px hsl(var(--sh-elev-color) / .1);
  --sh-elev-4: 0 1px 1px hsl(var(--sh-elev-color) / .09), 0 2px 2px hsl(var(--sh-elev-color) / .09), 0 4px 4px hsl(var(--sh-elev-color) / .09), 0 8px 8px hsl(var(--sh-elev-color) / .09), 0 16px 16px hsl(var(--sh-elev-color) / .09);
  --sh-elev-5: 0 1px 1px hsl(var(--sh-elev-color) / .08), 0 2px 2px hsl(var(--sh-elev-color) / .08), 0 4px 4px hsl(var(--sh-elev-color) / .08), 0 8px 8px hsl(var(--sh-elev-color) / .08), 0 16px 16px hsl(var(--sh-elev-color) / .08), 0 32px 32px hsl(var(--sh-elev-color) / .08);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 112px), 1fr));
  gap: 24px 20px;
  width: min(100%, 700px);
  padding: 28px 24px 40px;
  border-radius: 20px;
  background: #eef1f6;
}
.sh-elev__tile {
  display: grid;
  align-content: end;
  gap: 4px;
  min-height: 116px;
  padding: 16px 14px;
  border: 1px solid transparent;
  border-radius: 14px;
  background: #fff;
  color: #1c2433;
}
.sh-elev__tile strong {
  font-size: 16px;
  font-weight: 700;
}
.sh-elev__tile code {
  font-family: ui-monospace, Consolas, monospace;
  font-size: 12px;
  color: #34405a;
}
.sh-elev__tile span {
  font-size: 13px;
  line-height: 1.35;
  color: #566178;
}
.sh-elev__tile--1 {
  box-shadow: var(--sh-elev-1);
}
.sh-elev__tile--2 {
  box-shadow: var(--sh-elev-2);
}
.sh-elev__tile--3 {
  box-shadow: var(--sh-elev-3);
}
.sh-elev__tile--4 {
  box-shadow: var(--sh-elev-4);
}
.sh-elev__tile--5 {
  box-shadow: var(--sh-elev-5);
}

One shadow vs five layers

  • comma-separated shadows
  • doubling blur
  • warm shadow color

18°

Bergen

Light rain until 4 pm

One shadow0 16px 32px

18°

Bergen

Light rain until 4 pm

Five layers1, 2, 4, 8, 16px

Both widgets cover roughly the same area. The left one uses a single 16px offset with a 32px blur, which reads as a flat grey halo. The right one stacks five layers whose offset and blur double each time, so the shadow is dense near the edge and fades out softly, the way real contact shadows do.

View code
.sh-layer {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 32px 40px;
  width: min(100%, 560px);
  padding: 28px 16px 24px;
  border-radius: 20px;
  background: #ebe7df;
}
.sh-layer__item {
  display: grid;
  justify-items: center;
  gap: 18px;
  margin: 0;
}
.sh-layer__card {
  width: 150px;
  padding: 18px 16px;
  border: 1px solid transparent;
  border-radius: 18px;
  background: #fffdf8;
  color: #2a241b;
}
.sh-layer__card p {
  margin: 0;
}
.sh-layer__card--one {
  box-shadow: 0 16px 32px hsl(36deg 25% 20% / .2);
}
.sh-layer__card--five {
  box-shadow:
    0 1px 1px hsl(36deg 25% 20% / .09),
    0 2px 2px hsl(36deg 25% 20% / .09),
    0 4px 4px hsl(36deg 25% 20% / .09),
    0 8px 8px hsl(36deg 25% 20% / .09),
    0 16px 16px hsl(36deg 25% 20% / .09);
}
.sh-layer__temp {
  font-family: "Fraunces", serif;
  font-size: 44px;
  font-weight: 600;
  line-height: 1;
}
.sh-layer__place {
  margin-top: 10px;
  font-size: 16px;
  font-weight: 700;
}
.sh-layer__cond {
  margin-top: 2px;
  font-size: 13px;
  line-height: 1.35;
  color: #5c5242;
}
.sh-layer__cap {
  display: grid;
  justify-items: center;
  gap: 2px;
  font-size: 14px;
  font-weight: 600;
  color: #2a241b;
}
.sh-layer__cap code {
  font-family: ui-monospace, Consolas, monospace;
  font-size: 12px;
  font-weight: 400;
  color: #574d3e;
}

Shadows tinted to match the element

  • hsl() shadow color
  • negative spread
  • hue matching
Late Tram24 tracks
Ember Hour31 tracks
Night Index18 tracks
Night IndexBlack shadow

Each tile casts a shadow in its own hue at a lower lightness, with a negative spread so the color pools under the tile instead of spreading sideways. The last tile uses plain black at the same strength for comparison: the shadow turns muddy and the tile looks dirtier, not deeper.

View code
.sh-tint {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 112px), 1fr));
  gap: 26px 18px;
  width: min(100%, 640px);
  padding: 22px 16px 32px;
  box-sizing: border-box;
  border-radius: 20px;
  background: #f7f5f2;
}
.sh-tint__tile {
  display: grid;
  align-content: end;
  gap: 2px;
  aspect-ratio: 1;
  padding: 14px;
  border-radius: 16px;
  color: #fff;
}
.sh-tint__tile strong {
  font-family: "Fraunces", serif;
  font-size: 18px;
  font-weight: 600;
  line-height: 1.15;
}
.sh-tint__tile span {
  font-size: 13px;
}
.sh-tint__tile--teal {
  background: #0f766e;
  box-shadow: 0 2px 4px hsl(175deg 77% 18% / .3), 0 14px 24px -8px hsl(175deg 77% 26% / .7);
}
.sh-tint__tile--ember {
  background: #c2410c;
  box-shadow: 0 2px 4px hsl(21deg 88% 26% / .3), 0 14px 24px -8px hsl(21deg 88% 40% / .7);
}
.sh-tint__tile--violet {
  background: #6d28d9;
  box-shadow: 0 2px 4px hsl(263deg 70% 32% / .3), 0 14px 24px -8px hsl(263deg 70% 50% / .7);
}
.sh-tint__tile--black {
  background: #6d28d9;
  box-shadow: 0 2px 4px rgb(0 0 0 / .3), 0 14px 24px -8px rgb(0 0 0 / .7);
}
@media (forced-colors: active) {
  .sh-tint__tile {
    border: 1px solid CanvasText;
  }
}

Stacked notification cards

  • ::before / ::after
  • isolation
  • scaled layers

Parcelly

Your order is two stops away. Someone needs to be home to sign.

now

2 more notifications

The two cards behind the notification are pseudo-elements on the wrapper, scaled down and pushed lower, each with a lighter shadow than the one in front. isolation: isolate keeps their negative z-index inside the wrapper, so they slide behind the card without disappearing behind the page. Hover fans the stack out.

View code
.sh-stack {
  position: relative;
  isolation: isolate;
  width: min(100%, 340px);
  padding-bottom: 54px;
}
.sh-stack::before,
.sh-stack::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  height: calc(100% - 54px);
  border-radius: 18px;
  transform-origin: 50% 100%;
}
.sh-stack::before {
  z-index: -1;
  background: #f4f6fa;
  box-shadow: 0 1px 2px rgb(23 30 48 / .07), 0 4px 10px rgb(23 30 48 / .07);
  transform: translateY(12px) scale(.94);
}
.sh-stack::after {
  z-index: -2;
  background: #e9edf3;
  box-shadow: 0 1px 2px rgb(23 30 48 / .05), 0 3px 8px rgb(23 30 48 / .05);
  transform: translateY(24px) scale(.88);
}
.sh-stack__card {
  position: relative;
  display: grid;
  grid-template-columns: 40px 1fr auto;
  gap: 12px;
  align-items: start;
  padding: 14px 16px;
  border: 1px solid transparent;
  border-radius: 18px;
  background: #fff;
  box-shadow: 0 1px 2px rgb(23 30 48 / .1), 0 4px 8px rgb(23 30 48 / .08), 0 12px 24px rgb(23 30 48 / .08);
}
.sh-stack__icon {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: 11px;
  background: #1f6f5c;
  color: #fff;
  font-weight: 700;
}
.sh-stack__text p {
  margin: 0;
}
.sh-stack__from {
  font-size: 15px;
  font-weight: 700;
  color: #182032;
}
.sh-stack__msg {
  margin-top: 2px;
  font-size: 14px;
  line-height: 1.4;
  color: #3d4659;
}
.sh-stack__time {
  font-size: 12px;
  color: #5b6477;
}
.sh-stack__more {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  margin: 0;
  font-size: 13px;
  text-align: center;
  color: #4a5366;
}
@media (prefers-reduced-motion: no-preference) {
  .sh-stack::before,
  .sh-stack::after {
    transition: transform .3s ease;
  }
  .sh-stack:hover::before {
    transform: translateY(18px) scale(.95);
  }
  .sh-stack:hover::after {
    transform: translateY(34px) scale(.9);
  }
}

Print with lifted corners

  • rotated pseudo-elements
  • z-index: -1
  • isolation
Deep blue fjord between grey granite cliffs, seen from a crowded rock ledge high above the water
Lysefjord, early June

The curl is two invisible pseudo-elements tucked under the bottom corners, each rotated a few degrees and carrying a dark shadow. Only the part of each shadow that pokes out below the print shows, so the corners look lifted while the middle stays flat. The wrapper sets isolation: isolate so the negative z-index stays inside it.

View code
.sh-curl {
  isolation: isolate;
  display: grid;
  place-items: center;
  width: 100%;
  padding: 16px 12px 36px;
}
.sh-curl__print {
  position: relative;
  width: min(100%, 320px);
  margin: 0;
  padding: 12px 12px 14px;
  border: 1px solid transparent;
  background: #fdfcf9;
  box-shadow: 0 1px 2px rgb(40 34 24 / .14);
}
.sh-curl__print::before,
.sh-curl__print::after {
  content: "";
  position: absolute;
  z-index: -1;
  bottom: 16px;
  width: 46%;
  height: 24%;
  box-shadow: 0 16px 12px rgb(40 34 24 / .38);
}
.sh-curl__print::before {
  left: 10px;
  transform: rotate(-4deg);
}
.sh-curl__print::after {
  right: 10px;
  transform: rotate(4deg);
}
.sh-curl__print img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}
.sh-curl__print figcaption {
  margin-top: 10px;
  font-family: "Fraunces", serif;
  font-size: 15px;
  font-style: italic;
  text-align: center;
  color: #3b352c;
}

Inset, text and shape shadows

The other half of the toolbox: inner shadows for wells, text-shadow over photos and as a graphic style, hard offsets with no blur, and drop-shadow() for shapes that are not rectangles.

Inset wells for inputs and tracks

  • inset box-shadow
  • consistent inset on focus
  • outline focus

You are 16 EUR away from free delivery

Inputs and the progress track are pressed into the card with two soft inset shadows, which reads as somewhere to type. The focus state keeps the inset keyword and only lightens it, because mixing inset and outer shadows makes the change snap, and the focus indicator itself is a real outline.

View code
.sh-well {
  display: grid;
  gap: 16px;
  width: min(100%, 360px);
  padding: 22px 20px;
  border-radius: 18px;
  background: #fff;
  box-shadow: 0 1px 2px rgb(16 24 40 / .08), 0 6px 16px rgb(16 24 40 / .06);
  color: #1f2733;
}
.sh-well__field {
  display: grid;
  gap: 6px;
  font-size: 14px;
  font-weight: 600;
}
.sh-well__input {
  width: 100%;
  box-sizing: border-box;
  padding: 11px 12px;
  border: 1px solid #d3d8e0;
  border-radius: 10px;
  background: #edf0f4;
  box-shadow: inset 0 2px 4px rgb(16 24 40 / .1), inset 0 1px 1px rgb(16 24 40 / .08);
  color: #1f2733;
  font: inherit;
  font-weight: 400;
  resize: vertical;
  transition: background-color .2s ease;
}
.sh-well__input::placeholder {
  color: #5f6878;
}
.sh-well__input:focus {
  outline: 2px solid #2451b8;
  outline-offset: 2px;
  background: #f8f9fb;
  box-shadow: inset 0 1px 2px rgb(16 24 40 / .06), inset 0 1px 1px rgb(16 24 40 / .04);
}
.sh-well__progress {
  display: grid;
  gap: 8px;
}
.sh-well__label {
  margin: 0;
  font-size: 13px;
  color: #3e4757;
}
.sh-well__track {
  height: 12px;
  padding: 2px;
  box-sizing: border-box;
  border: 1px solid #d3d8e0;
  border-radius: 99px;
  background: #e6e9ee;
  box-shadow: inset 0 1px 3px rgb(16 24 40 / .2);
}
.sh-well__fill {
  display: block;
  width: 68%;
  height: 100%;
  border-radius: inherit;
  background: #2f7d55;
}
.sh-well__btn {
  justify-self: start;
  min-height: 44px;
  padding: 0 20px;
  border: 0;
  border-radius: 10px;
  background: #1f2733;
  color: #fff;
  font: inherit;
  font-weight: 600;
  box-shadow: 0 1px 2px rgb(16 24 40 / .2), 0 3px 6px rgb(16 24 40 / .12);
  cursor: pointer;
}
.sh-well__btn:focus-visible {
  outline: 2px solid #2451b8;
  outline-offset: 2px;
}

Text shadow over a photo

  • layered text-shadow
  • solid scrim
  • aria-pressed
Old brick and plaster houses along a canal, with white boats moored by a wooden jetty

Slow mornings on the canal

Four days of bikes, bakeries and boat tours, from 389 EUR.

A translucent solid band carries the contrast, and a three-layer text-shadow adds a soft dark edge around every glyph so bright spots in the photo, like the white boats, cannot bleed into the letters. Toggle the shadow to see how much edge definition it adds on its own.

View code
.sh-photo {
  position: relative;
  container-type: inline-size;
  width: min(100%, 600px);
  aspect-ratio: 4 / 3;
  overflow: hidden;
  border-radius: 18px;
  background: #3b4a55;
}
@media (max-width: 640px) {
  .sh-photo {
    aspect-ratio: 1;
  }
}
.sh-photo__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.sh-photo__copy {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  padding: 14px 18px 16px;
  background: rgb(10 14 22 / .4);
  color: #fff;
  text-shadow: 0 1px 1px rgb(0 0 0 / .6), 0 2px 6px rgb(0 0 0 / .5), 0 0 20px rgb(0 0 0 / .4);
}
.sh-photo--plain .sh-photo__copy {
  text-shadow: none;
}
.sh-photo__title {
  margin: 0;
  font-family: "Fraunces", serif;
  font-size: clamp(21px, 6cqi, 34px);
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: 0;
  color: #fff;
}
.sh-photo__text {
  margin: 6px 0 0;
  font-size: clamp(14px, 2.8cqi, 16px);
  line-height: 1.4;
}
.sh-photo__toggle {
  position: absolute;
  top: 12px;
  right: 12px;
  min-height: 36px;
  padding: 0 14px 0 34px;
  border: 0;
  border-radius: 99px;
  background: #fff;
  color: #1b2430;
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  box-shadow: 0 2px 6px rgb(0 0 0 / .25);
  cursor: pointer;
}
.sh-photo__toggle::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 12px;
  width: 14px;
  height: 14px;
  box-sizing: border-box;
  border: 2px solid #1b2430;
  border-radius: 4px;
  transform: translateY(-50%);
}
.sh-photo__toggle[aria-pressed="true"]::before {
  background: #1b2430;
  box-shadow: inset 0 0 0 2px #fff;
}
.sh-photo__toggle:focus-visible {
  outline: 3px solid #fff;
  outline-offset: 2px;
  box-shadow: 0 0 0 6px #1b2430;
}

Long shadow poster

  • stacked text-shadow
  • stacked box-shadow
  • overflow: hidden

Tide
Club

Open
daily

Surf lessons on the north beach, 7 am to dusk

A long shadow is the same hard shadow repeated one pixel further each time: 40 text-shadow values for the word and 30 box-shadow values for the badge. The poster clips them with overflow: hidden. It is heavy CSS, so keep it to one static graphic and never transition it.

View code
.sh-long {
  position: relative;
  container-type: inline-size;
  width: min(100%, 320px);
  aspect-ratio: 4 / 5;
  overflow: hidden;
  padding: 26px 24px;
  box-sizing: border-box;
  border-radius: 16px;
  background: #f2c14e;
  color: #1d2b36;
}
.sh-long__word {
  margin: 0;
  font-family: "Fraunces", serif;
  font-size: clamp(56px, 27cqi, 86px);
  font-weight: 700;
  line-height: .9;
  text-transform: uppercase;
  text-shadow: 1px 1px #d39e2a, 2px 2px #d39e2a, 3px 3px #d39e2a, 4px 4px #d39e2a, 5px 5px #d39e2a, 6px 6px #d39e2a, 7px 7px #d39e2a, 8px 8px #d39e2a, 9px 9px #d39e2a, 10px 10px #d39e2a, 11px 11px #d39e2a, 12px 12px #d39e2a, 13px 13px #d39e2a, 14px 14px #d39e2a, 15px 15px #d39e2a, 16px 16px #d39e2a, 17px 17px #d39e2a, 18px 18px #d39e2a, 19px 19px #d39e2a, 20px 20px #d39e2a, 21px 21px #d39e2a, 22px 22px #d39e2a, 23px 23px #d39e2a, 24px 24px #d39e2a, 25px 25px #d39e2a, 26px 26px #d39e2a, 27px 27px #d39e2a, 28px 28px #d39e2a, 29px 29px #d39e2a, 30px 30px #d39e2a, 31px 31px #d39e2a, 32px 32px #d39e2a, 33px 33px #d39e2a, 34px 34px #d39e2a, 35px 35px #d39e2a, 36px 36px #d39e2a, 37px 37px #d39e2a, 38px 38px #d39e2a, 39px 39px #d39e2a, 40px 40px #d39e2a;
}
.sh-long__badge {
  position: absolute;
  right: 24px;
  bottom: 84px;
  display: grid;
  place-items: center;
  width: 78px;
  height: 78px;
  margin: 0;
  border-radius: 50%;
  background: #1d2b36;
  color: #f2c14e;
  font-size: 13px;
  font-weight: 700;
  line-height: 1.1;
  text-align: center;
  text-transform: uppercase;
  box-shadow: 1px 1px 0 #d39e2a, 2px 2px 0 #d39e2a, 3px 3px 0 #d39e2a, 4px 4px 0 #d39e2a, 5px 5px 0 #d39e2a, 6px 6px 0 #d39e2a, 7px 7px 0 #d39e2a, 8px 8px 0 #d39e2a, 9px 9px 0 #d39e2a, 10px 10px 0 #d39e2a, 11px 11px 0 #d39e2a, 12px 12px 0 #d39e2a, 13px 13px 0 #d39e2a, 14px 14px 0 #d39e2a, 15px 15px 0 #d39e2a, 16px 16px 0 #d39e2a, 17px 17px 0 #d39e2a, 18px 18px 0 #d39e2a, 19px 19px 0 #d39e2a, 20px 20px 0 #d39e2a, 21px 21px 0 #d39e2a, 22px 22px 0 #d39e2a, 23px 23px 0 #d39e2a, 24px 24px 0 #d39e2a, 25px 25px 0 #d39e2a, 26px 26px 0 #d39e2a, 27px 27px 0 #d39e2a, 28px 28px 0 #d39e2a, 29px 29px 0 #d39e2a, 30px 30px 0 #d39e2a;
}
.sh-long__meta {
  position: absolute;
  right: 24px;
  bottom: 22px;
  left: 24px;
  margin: 0;
  font-size: 14px;
  font-weight: 600;
  line-height: 1.35;
}
@media (forced-colors: active) {
  .sh-long {
    border: 1px solid CanvasText;
  }
}

Neo-brutalist hard offset

  • zero-blur shadow
  • press state
  • border + shadow

Risograph zine fair

Saturday 14 November, Dock Hall 3

Forty small presses, one warehouse and a free tote for the first hundred visitors.

Zero blur turns box-shadow into a solid offset block. Pair it with a thick border in the same color and the card looks printed rather than lit. Pressing a button moves it into its own shadow, which is the whole interaction: the shadow shrinks to 0 while the button shifts by the same amount.

View code
.sh-brut {
  width: min(calc(100% - 10px), 380px);
  margin: 0 10px 10px 0;
  padding: 22px 22px 24px;
  box-sizing: border-box;
  border: 3px solid #111;
  border-radius: 14px;
  background: #fff2a8;
  color: #111;
  box-shadow: 8px 8px 0 #111;
}
.sh-brut__title {
  margin: 0;
  font-family: "Fraunces", serif;
  font-size: 26px;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: 0;
  color: #111;
}
.sh-brut__date {
  display: inline-block;
  margin: 10px 0 0;
  padding: 3px 8px;
  border: 2px solid #111;
  border-radius: 6px;
  background: #fff;
  font-size: 13px;
  font-weight: 700;
}
.sh-brut__text {
  margin: 12px 0 0;
  font-size: 15px;
  line-height: 1.5;
}
.sh-brut__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 18px;
}
.sh-brut__btn {
  min-height: 44px;
  padding: 0 18px;
  border: 2px solid #111;
  border-radius: 10px;
  background: #ff7ab8;
  color: #111;
  font: inherit;
  font-weight: 700;
  box-shadow: 4px 4px 0 #111;
  cursor: pointer;
}
.sh-brut__btn--plain {
  background: #fff;
}
.sh-brut__btn:hover {
  box-shadow: 6px 6px 0 #111;
  transform: translate(-2px, -2px);
}
.sh-brut__btn:active {
  box-shadow: 0 0 0 #111;
  transform: translate(4px, 4px);
}
.sh-brut__btn:focus-visible {
  outline: 3px solid #1d4ed8;
  outline-offset: 3px;
}
@media (prefers-reduced-motion: no-preference) {
  .sh-brut__btn {
    transition: transform .12s ease, box-shadow .12s ease;
  }
}

drop-shadow() vs box-shadow

  • filter: drop-shadow()
  • clip-path
  • border triangle
Saved to drafts

box-shadow skips the tail

Saved to drafts

drop-shadow() follows it

30% off

box-shadow is clipped away

30% off

drop-shadow() on the parent

box-shadow traces the border box, so it ignores the bubble tail and is cut away completely by clip-path. drop-shadow() works on the rendered pixels instead, so it follows the tail, and on the clipped tag it works once it moves to the parent. The blur value is halved for drop-shadow() because its third value is a standard deviation, not a blur radius.

View code
.sh-drop {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 30px 18px;
  width: min(100%, 520px);
  padding: 8px 0;
}
.sh-drop__cell {
  display: grid;
  justify-items: center;
  align-content: start;
  gap: 18px;
}
.sh-drop__bubble {
  position: relative;
  padding: 12px 16px;
  border-radius: 12px;
  background: #fff;
  color: #1d2433;
  font-size: 14px;
  font-weight: 600;
  text-align: center;
}
.sh-drop__bubble::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  margin-left: -10px;
  border: 10px solid transparent;
  border-top-color: #fff;
  border-bottom: 0;
}
.sh-drop__bubble--box {
  box-shadow: 0 6px 12px rgb(15 23 42 / .3);
}
.sh-drop__bubble--filter {
  filter: drop-shadow(0 6px 6px rgb(15 23 42 / .3));
}
.sh-drop__tag {
  position: relative;
  display: grid;
  place-items: center;
  width: 128px;
  height: 52px;
  padding-left: 18px;
  box-sizing: border-box;
  background: #b93b0b;
  color: #fff;
  font-size: 19px;
  font-weight: 700;
  clip-path: polygon(0 50%, 20% 0, 100% 0, 100% 100%, 20% 100%);
}
.sh-drop__tag::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 17px;
  width: 9px;
  height: 9px;
  margin-top: -4.5px;
  border-radius: 50%;
  background: #eef0f3;
}
.sh-drop__tag--box {
  box-shadow: 0 6px 12px rgb(15 23 42 / .35);
}
.sh-drop__wrap {
  filter: drop-shadow(0 6px 6px rgb(15 23 42 / .35));
}
.sh-drop__cap {
  margin: 0;
  font-size: 13px;
  line-height: 1.4;
  text-align: center;
  color: #2c3443;
}
.sh-drop__cap code {
  font-family: ui-monospace, Consolas, monospace;
  font-size: 12px;
  color: #1d2433;
}

Shadows that react

Shadows that respond to hover, focus and scroll. Each one changes opacity or a static value instead of animating the shadow itself, and keeps a non-shadow fallback for forced colors.

Hover lift with a pre-rendered shadow

  • ::after opacity
  • transform
  • :focus-within

Ridge walk above the tarns

14 km, 6 hours, hard

Hover, focus or scroll inside the demo.

The big hover shadow is painted once on a pseudo-element and starts at opacity 0. Hover or keyboard focus fades it in and lifts the card with transform, so the browser animates two compositor-friendly properties instead of repainting a five-layer shadow on every frame.

View code
.sh-lift {
  position: relative;
  isolation: isolate;
  width: min(100%, 300px);
  margin: 6px 0 14px;
  border: 1px solid transparent;
  border-radius: 16px;
  background: #fff;
  box-shadow: 0 1px 2px rgb(24 33 27 / .08), 0 2px 6px rgb(24 33 27 / .06);
}
.sh-lift::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  box-shadow: 0 2px 4px rgb(24 33 27 / .06), 0 8px 16px rgb(24 33 27 / .08), 0 16px 32px rgb(24 33 27 / .1), 0 24px 48px rgb(24 33 27 / .1);
  opacity: 0;
  pointer-events: none;
  transition: opacity .3s ease;
}
.sh-lift:hover::after,
.sh-lift:focus-within::after {
  opacity: 1;
}
.sh-lift__img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  border-radius: 15px 15px 0 0;
}
.sh-lift__body {
  padding: 16px 18px 18px;
}
.sh-lift__title {
  margin: 0;
  font-family: "Fraunces", serif;
  font-size: 20px;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0;
  color: #18211b;
}
.sh-lift__meta {
  margin: 4px 0 14px;
  font-size: 14px;
  color: #4b5a50;
}
.sh-lift__btn {
  min-height: 44px;
  padding: 0 18px;
  border: 0;
  border-radius: 10px;
  background: #2d5a43;
  color: #fff;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.sh-lift__btn:focus-visible {
  outline: 3px solid #18211b;
  outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .sh-lift {
    transition: transform .3s ease;
  }
  .sh-lift:hover,
  .sh-lift:focus-within {
    transform: translateY(-4px);
  }
}

Box-shadow focus ring with an outline fallback

  • spread-only shadow
  • transparent outline
  • forced-colors
Role

Hover, focus or scroll inside the demo.

The visible ring is two spread-only shadows: a white gap and a blue band that follow the border radius. Forced colors mode removes box-shadow entirely, so every control also gets a transparent outline, which the forced palette turns into a visible one, plus an explicit Highlight outline inside a forced-colors media query.

View code
.sh-ring {
  display: grid;
  gap: 8px;
  width: min(100%, 360px);
  padding: 22px 20px;
  box-sizing: border-box;
  border-radius: 18px;
  background: #fff;
  box-shadow: 0 1px 2px rgb(15 23 42 / .08), 0 6px 16px rgb(15 23 42 / .06);
  color: #172033;
}
.sh-ring__label {
  padding: 0;
  font-size: 14px;
  font-weight: 600;
}
.sh-ring__input {
  width: 100%;
  box-sizing: border-box;
  min-height: 44px;
  padding: 0 12px;
  border: 1px solid #8a94a6;
  border-radius: 10px;
  background: #fff;
  color: #172033;
  font: inherit;
}
.sh-ring__input::placeholder {
  color: #5f6878;
}
.sh-ring__roles {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 10px 0 0;
  padding: 0;
  border: 0;
}
.sh-ring__roles legend {
  width: 100%;
  margin-bottom: 8px;
}
.sh-ring__pill {
  position: relative;
}
.sh-ring__pill input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}
.sh-ring__pill span {
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  padding: 0 16px;
  border: 1px solid #8a94a6;
  border-radius: 99px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
.sh-ring__pill input:checked + span {
  border-color: #172033;
  background: #172033;
  color: #fff;
}
.sh-ring__row {
  display: flex;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 14px;
}
.sh-ring__btn {
  min-height: 44px;
  padding: 0 18px;
  border: 1px solid #1d4ed8;
  border-radius: 10px;
  background: #1d4ed8;
  color: #fff;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.sh-ring__btn--plain {
  border-color: #8a94a6;
  background: #fff;
  color: #172033;
}
.sh-ring__input:focus-visible,
.sh-ring__btn:focus-visible,
.sh-ring__pill input:focus-visible + span {
  outline: 2px solid transparent;
  outline-offset: 2px;
  box-shadow: 0 0 0 2px #fff, 0 0 0 5px #1d4ed8;
}
@media (forced-colors: active) {
  .sh-ring__input:focus-visible,
  .sh-ring__btn:focus-visible,
  .sh-ring__pill input:focus-visible + span {
    outline: 3px solid Highlight;
  }
  .sh-ring__pill input:checked + span {
    border-width: 3px;
  }
}

Inner shadow scroll hints

  • scroll-state(scrollable)
  • sticky edges
  • inset-style shadow

Release notes

  • 4.12Shadow tokens now follow the theme hue.
  • 4.11Dialogs use the level 5 elevation token.
  • 4.10Focus rings keep an outline in forced colors.
  • 4.9Sticky headers gain a shadow once stuck.
  • 4.8Card hover lift no longer repaints the shadow.
  • 4.7Dark theme raises surfaces instead of darkening shadows.
  • 4.6Inputs switch to inset wells.
  • 4.5Removed the last -webkit-box-shadow lines.

Hover, focus or scroll inside the demo.

Two zero-height sticky edges sit at the start and end of the scroll area, each carrying a soft spread shadow. scroll-state(scrollable: top) and scroll-state(scrollable: bottom) show each edge only when there is more content in that direction. Without scroll-state queries, the bottom hint stays on as a static cue.

View code
.sh-hint {
  width: min(100%, 340px);
  padding: 18px 0 8px;
  border-radius: 18px;
  background: #fff;
  box-shadow: 0 1px 2px rgb(16 24 40 / .08), 0 6px 16px rgb(16 24 40 / .06);
  color: #1b2333;
}
.sh-hint__title {
  margin: 0 20px 10px;
  font-size: 16px;
  font-weight: 700;
}
.sh-hint__scroll {
  position: relative;
  max-height: 228px;
  overflow-y: auto;
  overscroll-behavior: contain;
  border-top: 1px solid #e6e9ef;
  border-bottom: 1px solid #e6e9ef;
}
.sh-hint__scroll:focus-visible {
  outline: 3px solid #2451b8;
  outline-offset: -3px;
}
.sh-hint__edge {
  position: sticky;
  z-index: 1;
  display: block;
  height: 0;
  box-shadow: 0 0 12px 6px rgb(16 24 40 / .2);
  pointer-events: none;
  transition: opacity .2s ease;
}
.sh-hint__edge--top {
  top: 0;
  opacity: 0;
}
.sh-hint__edge--bottom {
  bottom: 0;
  opacity: 1;
}
.sh-hint__list {
  margin: 0;
  padding: 0;
  list-style: none;
}
.sh-hint__list li {
  display: grid;
  grid-template-columns: 44px 1fr;
  gap: 8px;
  margin: 0;
  padding: 12px 20px;
  border-bottom: 1px solid #eef1f5;
  font-size: 14px;
  line-height: 1.4;
}
.sh-hint__list li:last-child {
  border-bottom: 0;
}
.sh-hint__list strong {
  font-family: ui-monospace, Consolas, monospace;
  font-size: 13px;
  color: #2451b8;
}
.sh-hint__list span {
  color: #3e4757;
}
@supports (container-type: scroll-state) {
  .sh-hint__scroll {
    container-type: scroll-state;
  }
  .sh-hint__edge--bottom {
    opacity: 0;
  }
  @container scroll-state(scrollable: top) {
    .sh-hint__edge--top {
      opacity: 1;
    }
  }
  @container scroll-state(scrollable: bottom) {
    .sh-hint__edge--bottom {
      opacity: 1;
    }
  }
}

Shadows on dark UI

Black shadows vanish on near-black backgrounds. On dark interfaces, depth comes from lighter surfaces and the occasional soft, solid-color glow.

Subtle status glow on a dark panel

  • spread ring + blur
  • solid color glow
  • text labels

Edge regions

  • FrankfurtHealthy, 18 ms
  • SingaporeHealthy, 64 ms
  • OregonSlow, 142 ms
  • Sao PauloOffline

Each status dot gets a tight translucent ring plus a short blur in the same solid color, which reads as a lit LED without turning the panel into neon. The button glows only on hover and focus. Status is also written out in text, because the glow is decoration and disappears in forced colors.

View code
.sh-glow {
  width: min(100%, 380px);
  padding: 18px 18px 8px;
  box-sizing: border-box;
  border: 1px solid #252c37;
  border-radius: 16px;
  background: #121720;
  color: #e6edf5;
}
.sh-glow__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 8px;
}
.sh-glow__title {
  margin: 0;
  font-size: 16px;
  font-weight: 700;
}
.sh-glow__btn {
  min-height: 40px;
  padding: 0 16px;
  border: 1px solid #34d399;
  border-radius: 10px;
  background: #34d399;
  color: #052e1f;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.sh-glow__btn:hover,
.sh-glow__btn:focus-visible {
  box-shadow: 0 0 0 3px rgb(52 211 153 / .25), 0 0 18px 2px rgb(52 211 153 / .35);
}
.sh-glow__btn:focus-visible {
  outline: 2px solid #e6edf5;
  outline-offset: 2px;
}
.sh-glow__list {
  margin: 0;
  padding: 0;
  list-style: none;
}
.sh-glow__list li {
  display: grid;
  grid-template-columns: 10px 1fr auto;
  gap: 12px;
  align-items: center;
  margin: 0;
  padding: 12px 0;
  border-top: 1px solid #222936;
  font-size: 14px;
}
.sh-glow__dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
}
.sh-glow__dot--ok {
  background: #34d399;
  box-shadow: 0 0 0 3px rgb(52 211 153 / .18), 0 0 10px 1px rgb(52 211 153 / .5);
}
.sh-glow__dot--warn {
  background: #fbbf24;
  box-shadow: 0 0 0 3px rgb(251 191 36 / .18), 0 0 10px 1px rgb(251 191 36 / .5);
}
.sh-glow__dot--down {
  background: #f87171;
  box-shadow: 0 0 0 3px rgb(248 113 113 / .18), 0 0 10px 1px rgb(248 113 113 / .5);
}
.sh-glow__state {
  font-size: 13px;
  color: #a3afbf;
}
.sh-glow__state--warn {
  color: #fcd34d;
}
.sh-glow__state--down {
  color: #fca5a5;
}
@media (prefers-reduced-motion: no-preference) {
  .sh-glow__btn {
    transition: box-shadow .2s ease;
  }
}
@media (forced-colors: active) {
  .sh-glow__dot {
    border: 2px solid CanvasText;
  }
}

Dark mode: lighter surfaces, not darker shadows

  • surface tint per level
  • inset top highlight
  • small shadow
Invoice 2041Due Friday, 1,280 EUR
  • Download PDF
  • Duplicate

Shadows only

Invoice 2041Due Friday, 1,280 EUR
  • Download PDF
  • Duplicate

Lighter surfaces

Both columns use the same black shadows. On the left that is all there is, and the card and menu barely separate from the page. On the right each level also gets a lighter surface and a 1px inset highlight on its top edge, so the order of the layers stays obvious while the shadow only supports it.

View code
.sh-darkel {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr));
  gap: 22px 18px;
  width: min(100%, 520px);
}
.sh-darkel__col {
  display: grid;
  gap: 12px;
}
.sh-darkel__page {
  position: relative;
  min-height: 190px;
  padding: 16px 14px;
  border: 1px solid #262b33;
  border-radius: 14px;
  background: #121418;
  color: #e8ecf1;
}
.sh-darkel__card {
  display: grid;
  gap: 4px;
  padding: 14px;
  border: 1px solid transparent;
  border-radius: 12px;
  background: #121418;
  box-shadow: 0 2px 4px rgb(0 0 0 / .5), 0 8px 16px rgb(0 0 0 / .4);
}
.sh-darkel__card strong {
  font-size: 15px;
}
.sh-darkel__card span {
  font-size: 13px;
  color: #a9b3c1;
}
.sh-darkel__menu {
  position: absolute;
  right: 12px;
  bottom: 14px;
  width: 128px;
  margin: 0;
  padding: 6px;
  border: 1px solid transparent;
  border-radius: 10px;
  list-style: none;
  background: #121418;
  box-shadow: 0 4px 8px rgb(0 0 0 / .5), 0 16px 32px rgb(0 0 0 / .45);
}
.sh-darkel__menu li {
  margin: 0;
  padding: 7px 8px;
  border-radius: 6px;
  font-size: 13px;
}
.sh-darkel__menu li:first-child {
  background: rgb(255 255 255 / .06);
}
.sh-darkel__page--lit .sh-darkel__card {
  background: #1d2128;
  box-shadow: inset 0 1px 0 rgb(255 255 255 / .06), 0 2px 4px rgb(0 0 0 / .5), 0 8px 16px rgb(0 0 0 / .4);
}
.sh-darkel__page--lit .sh-darkel__menu {
  background: #2a2f38;
  box-shadow: inset 0 1px 0 rgb(255 255 255 / .09), 0 4px 8px rgb(0 0 0 / .5), 0 16px 32px rgb(0 0 0 / .45);
}
.sh-darkel__cap {
  margin: 0;
  font-size: 13px;
  font-weight: 600;
  text-align: center;
  color: #c9d1dc;
}

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).

  1. Start with offset-x and offset-y. Negative values push the shadow left and up.
  2. Blur radius comes next. It defaults to 0, which gives a sharp edge, and it can’t be negative.
  3. Spread only matters when the shadow has to grow or shrink around the box, so skip it otherwise.
  4. Pick a color, ideally rgba with a low alpha. Leave it out and the shadow falls back to the color property.
  5. 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.

ToolShadow followsAccepted valuesBest for
box-shadowBorder box and border-radiusOffsets, blur, spread, color, insetCards, buttons, inputs
text-shadowGlyph outlines and decorationOffsets, blur, colorHeadings, glow, outlines
drop-shadow()Alpha channel of the rendered elementOffsets, blur, colorTransparent 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).

  1. Pick one light direction and keep the x:y offset ratio fixed for every shadow on the page. Josh Comeau uses 1:2.
  2. Take the shadow color from the background hue, with lower saturation and lightness, and store it in a custom property.
  3. Write layers where offset and blur double each time, such as 1px 2px 2px, 2px 4px 4px and 4px 8px 8px.
  4. Split the alpha across the layers. Every added layer needs a lower alpha to keep the total strength.
  5. 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.