---
name: depth-shadow-stacking
description: Use when you want "Professional, refined, subtle" - Uses multiple layered shadows to simulate realistic elevation.
---

# Depth Shadow Stacking

> **Category:** Glass / Depth  -  **Personality:** Professional, refined, subtle
>
> **Best use:** Enterprise UI, cards, modals
>
> **Ratings:** Professional 5/5 - Casual 2/5 - Exotic 2/5 - Visual impact 3/5 - Difficulty 2/5 - Performance cost 2/5

## What it is

A single `box-shadow` looks fake — real objects don't cast one hard, even shadow. Depth Shadow Stacking composes each elevation from **a stack of several shadow layers**: a tight near-opaque one for contact/ambient occlusion, then progressively larger, softer, fainter ones for the light spilling around the object. Step the offset and blur up an exponential curve and you get a smooth, photoreal shadow. Define a handful of named tiers (resting → overlay) and you have a coherent elevation system — exactly what enterprise dashboards, cards and modals use to signal hierarchy.

## Dependencies / CDN

None - pure CSS for the effect. The optional JS just swaps the tier and drives a modal. The demo loads two display/body fonts (optional):

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700&family=Schibsted+Grotesk:wght@500;600;700;800&display=swap" rel="stylesheet">
```

## HTML

A surface that carries an elevation tier, plus a control that swaps tiers:

```html
<div class="stage">
  <!-- the live, controllable surface -->
  <article class="card liftable e2" id="card">
    <span class="label">Workspace plan</span>
    <strong class="price">$1,290</strong>
    <button class="btn" id="open">Review upgrade</button>
  </article>

  <!-- the full scale, at a glance -->
  <div class="ladder">
    <div class="card rung liftable e1" data-elev="1">Resting</div>
    <div class="card rung liftable e2" data-elev="2">Raised</div>
    <div class="card rung liftable e3" data-elev="3">Floating</div>
    <div class="card rung liftable e4" data-elev="4">Lifted</div>
    <div class="card rung liftable e5" data-elev="5">Overlay</div>
  </div>
</div>
```

## CSS

```css
/* 1) THE EFFECT — each tier is a STACK of shadows, not one.
   Tinted ink (cool navy, not black) + a constant lit top edge.
   Offset & blur grow ~1.7x per layer; alpha stays low (~.04–.07). */
.stage{
  --sh: 222 45% 20%;                       /* shadow colour as HSL channels */
  --hl: inset 0 1px 0 hsl(0 0% 100% / .7); /* lit top edge */
  --e1: 0 1px 1.1px hsl(var(--sh)/.05),
        0 1.8px 2px -1.2px hsl(var(--sh)/.06),
        0 4.2px 4.8px -2.5px hsl(var(--sh)/.07), var(--hl);
  --e2: 0 1px 1.1px hsl(var(--sh)/.045),
        0 2.7px 3.1px -.6px hsl(var(--sh)/.05),
        0 6px 6.8px -1.2px hsl(var(--sh)/.055),
        0 12.5px 14px -1.9px hsl(var(--sh)/.06), var(--hl);
  --e3: 0 1px 1.1px hsl(var(--sh)/.04), 0 3px 3.4px -.4px hsl(var(--sh)/.045),
        0 6px 6.8px -.8px hsl(var(--sh)/.05), 0 11px 12.4px -1.2px hsl(var(--sh)/.052),
        0 19.5px 22px -1.6px hsl(var(--sh)/.055), 0 33px 37px -2px hsl(var(--sh)/.06), var(--hl);
  --e4: 0 1px 1.2px hsl(var(--sh)/.035), 0 4px 4.5px -.3px hsl(var(--sh)/.04),
        0 8px 9px -.6px hsl(var(--sh)/.043), 0 14px 16px -.9px hsl(var(--sh)/.046),
        0 24px 27px -1.2px hsl(var(--sh)/.05), 0 41px 46px -1.5px hsl(var(--sh)/.053),
        0 64px 72px -1.8px hsl(var(--sh)/.057), var(--hl);
  --e5: 0 2px 2.2px hsl(var(--sh)/.03), 0 5px 6px -.2px hsl(var(--sh)/.034),
        0 10px 12px -.5px hsl(var(--sh)/.038), 0 18px 21px -.8px hsl(var(--sh)/.04),
        0 31px 36px -1.1px hsl(var(--sh)/.044), 0 52px 60px -1.4px hsl(var(--sh)/.048),
        0 86px 98px -1.7px hsl(var(--sh)/.053), 0 132px 150px -2px hsl(var(--sh)/.06), var(--hl);

  /* shadows only read on a LIGHT surface */
  background: radial-gradient(135% 120% at 50% -20%, #f6f8fc, #e7ecf5 60%, #dfe5f0);
  position: relative; overflow: hidden; isolation: isolate; border-radius: 24px;
}

/* 2) Map each tier to a class. Pair the shadow with a small lift so the
   change feels physical, and transition both for a smooth rise. */
.card    { background:#fff; border:1px solid rgba(15,22,38,.07); border-radius:18px }
.liftable{ transition: box-shadow .5s cubic-bezier(.22,.61,.18,1),
                       transform  .5s cubic-bezier(.22,.61,.18,1) }
.e1{ box-shadow:var(--e1); transform:translateY(0) }
.e2{ box-shadow:var(--e2); transform:translateY(-2px) }
.e3{ box-shadow:var(--e3); transform:translateY(-5px) }
.e4{ box-shadow:var(--e4); transform:translateY(-9px) }
.e5{ box-shadow:var(--e5); transform:translateY(-14px) }

/* 3) Hover = climb one tier */
.rung:hover{ transform:translateY(-8px) }
.rung[data-elev="1"]:hover{ box-shadow:var(--e2) }
.rung[data-elev="2"]:hover{ box-shadow:var(--e3) }
.rung[data-elev="3"]:hover{ box-shadow:var(--e4) }
.rung[data-elev="4"]:hover,
.rung[data-elev="5"]:hover{ box-shadow:var(--e5) }

@media (prefers-reduced-motion: reduce){
  .liftable{ transition:none } .rung:hover{ transform:none }
}
```

## JavaScript

Optional — swap the tier on the live card, and run a modal at the top tier. The effect itself is pure CSS; hover/elevation work with no JS.

```js
var card = document.getElementById('card');
document.querySelectorAll('.seg').forEach(function(b){
  b.addEventListener('click', function(){
    card.className = 'card liftable e' + b.dataset.elev;   // swap the tier class
    document.querySelectorAll('.seg').forEach(function(s){
      var on = s === b;
      s.classList.toggle('is-active', on);
      s.setAttribute('aria-pressed', on ? 'true' : 'false');
    });
  });
});

// Modal: open at the highest tier (--elevation-5), close on Esc / backdrop.
var modal = document.getElementById('modal'), last = null;
function open(){ last = document.activeElement; modal.classList.add('is-open');
  modal.setAttribute('aria-hidden','false'); requestAnimationFrame(function(){ modal.querySelector('button').focus(); });
  document.addEventListener('keydown', onKey); }
function close(){ modal.classList.remove('is-open'); modal.setAttribute('aria-hidden','true');
  document.removeEventListener('keydown', onKey); if(last) last.focus(); }
function onKey(e){ if(e.key === 'Escape') close(); }
modal.addEventListener('click', function(e){ if(e.target === modal) close(); });
```

## How it works

- **Stacking is the whole trick.** Layer 1 is small/dark (the contact shadow); each later layer doubles roughly in offset and blur while alpha stays around `.04–.07`. Summed, they approximate how real light wraps an object — soft and gradated, never a flat smudge.
- **Higher tier = more layers, pushed further.** Resting is 3 short layers; Overlay is 8 layers reaching ~130px. The growth curve is what makes a modal feel like it's floating high above the page.
- **Tinted ink, not black.** `hsl(222 45% 20% / a)` (cool navy) reads as a real shadow; pure `#000` looks like dirt.
- **A constant lit top edge.** `inset 0 1px 0 rgba(255,255,255,.7)` is appended to every tier so the surface always catches light on its top edge.
- **Lift + shadow together.** Each tier also nudges the element up a few px, so changing elevation feels physical, not just darker.

## Customization

- **Tune the whole system from one variable:** change `--sh` hue/lightness to re-tint every shadow at once (warm the hue for a sunset UI, drop lightness for a denser shadow).
- **More/less depth:** add or remove layers, or scale every offset/blur by a constant. Tools like *smoothshadows.com* or *shadows.brumm.af* generate stacks you can paste in.
- **Strength:** raise each layer's alpha by ~`.01` for a punchier shadow; lower it for whisper-soft enterprise calm.
- **Direction:** add a small X offset to every layer for a side-lit look, or keep X at 0 for top-down (the safest default).
- **Tiers:** rename/extend (`--e6` for toasts/tooltips). Keep the count small (4–6) so hierarchy stays legible.

## Accessibility & performance

- **Animating `box-shadow` repaints every layer** and is the costly path. For one-off state changes (hover, click) it's fine. For continuous/looping motion, keep the shadow static on the element and animate the **opacity of a `::after` pseudo** that holds the bigger shadow instead — that composites on the GPU.
- `transform` is used for the lift (cheap, GPU-composited); keep `will-change` only on elements you actually animate.
- Respect `prefers-reduced-motion`: drop the transitions and the hover transform (the demo does).
- Elevation is a *visual* cue only — never rely on it alone to convey state. The demo's tier control uses real `<button aria-pressed>` and an `aria-live` readout; the modal uses `role="dialog" aria-modal="true"`, focus-in/return, Esc and backdrop close.

## Gotchas

- **Shadows need a light background.** On a dark surface a dark shadow is invisible — switch to a light/elevation glow (a faint light-coloured shadow) or lean on borders/overlays for dark UIs.
- **`overflow:hidden` on an ancestor clips the shadow.** A scroll container or rounded panel will crop the soft edges; give the stage padding or let the shadow's owner sit outside the clip.
- **One hard shadow betrays the fake.** The realism comes entirely from *multiple* layers — don't collapse the stack to save bytes.
- **Don't stack dozens of heavily-shadowed cards.** Each is a multi-layer paint; on long lists reuse a couple of shared tier classes rather than unique shadows per row.
- **Tint, don't blacken.** High-alpha pure-black shadows make a clean UI look grubby; keep alpha low and the colour cool.
