---
name: layered-depth-cards
description: Use when you want "Professional, structured, modern" - Stacks cards with shadows, offsets, and opacity to create spatial hierarchy.
---

# Layered Depth Cards

> **Category:** Glass / Depth  -  **Personality:** Professional, structured, modern
>
> **Best use:** Feature sections, pricing, portfolios
>
> **Ratings:** Professional 5/5 - Casual 3/5 - Exotic 3/5 - Visual impact 4/5 - Difficulty 2/5 - Performance cost 2/5

## What it is

Layered Depth Cards stack two or more cards on the same footprint and push the
back ones up/back with a small offset, a slightly smaller scale and lower opacity,
so the set reads as a physical pile with a clear front-to-back hierarchy. The depth
itself comes from a **layered, multi-stop `box-shadow`** (many soft shadows at growing
blur/spread instead of one) — the closer a card is to the viewer, the deeper its cast.
Put the whole pile inside a `perspective` + `transform-style: preserve-3d` parent and
each layer can be separated on a real Z-axis, so a tiny pointer tilt makes the plates
drift apart convincingly. It's a calm, structured way to say "this is the important one,
and there's more behind it" — ideal for feature sections, pricing and portfolio decks.

## Dependencies / CDN

None - pure CSS for the effect; ~15 lines of vanilla JS for the optional pointer tilt.
The demo loads three Google Fonts (display / body / mono) — swap freely:

```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&family=Schibsted+Grotesk:wght@500;700;800&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
```

## HTML

Back-to-front order. The **front** card is in normal flow and sizes the deck; the
back plates are `position:absolute; inset:0` so they share that exact footprint.

```html
<div class="deckwrap">           <!-- owns the perspective -->
  <div class="deck">             <!-- preserve-3d; JS tilts this -->
    <article class="card card--back" aria-hidden="true">
      <header class="peek"><span class="peek-idx">02</span><span class="peek-t">Vance Tower — Madrid</span></header>
    </article>
    <article class="card card--mid" aria-hidden="true">
      <header class="peek"><span class="peek-idx">03</span><span class="peek-t">Marin Atelier — Porto</span></header>
    </article>
    <article class="card card--front">
      <div class="thumb"><img src="/assets/img/architecture-01.jpg" alt="Helix Pavilion"><span class="badge">Featured</span></div>
      <div class="body">
        <h3 class="title">Helix Pavilion</h3>
        <p class="desc">A spiralling civic atrium wrapped in board-formed concrete.</p>
        <a class="link" href="#">View case study →</a>
      </div>
    </article>
    <span class="chip">RIBA Award · 2025</span> <!-- floats forward on its own Z plane -->
  </div>
</div>
```

## CSS

```css
/* 1) THE LAYERED SHADOW — the heart of the effect. Many soft stops, growing
      blur + a negative spread on the far ones, = one believable soft cast. */
:root{
  --sh-front:
    0 .9px .8px rgba(26,22,30,.05),  0 2.2px 2px rgba(26,22,30,.07),
    0 4.6px 4.2px rgba(26,22,30,.085), 0 8.8px 8px rgba(26,22,30,.10),
    0 16.5px 15px rgba(26,22,30,.12), 0 31px 29px -3px rgba(26,22,30,.15),
    0 58px 62px -10px rgba(26,22,30,.22);
  --sh-mid:  0 8px 16px -6px rgba(26,22,30,.12), 0 20px 28px -12px rgba(26,22,30,.14);
  --sh-back: 0 6px 14px -8px rgba(26,22,30,.10), 0 16px 22px -14px rgba(26,22,30,.12);
}

/* 2) PERSPECTIVE on the parent, preserve-3d on the moving deck */
.deckwrap{display:flex; justify-content:center; perspective:1300px; perspective-origin:50% 38%}
.deck{position:relative; width:358px; max-width:100%;
  transform-style:preserve-3d; transition:transform .3s cubic-bezier(.22,.61,.36,1)}

/* 3) THE STACK — same footprint, offset up + scaled down + faded as it recedes.
      transform-origin:top center keeps the peeking top edge predictable. */
.card{border-radius:20px; background:#fff; border:1px solid rgba(28,27,34,.085)}
.card--front{position:relative; z-index:3; transform:translateZ(28px);  box-shadow:var(--sh-front);
  transition:transform .55s cubic-bezier(.22,.61,.36,1), box-shadow .55s}
.card--mid{position:absolute; inset:0; z-index:2; transform-origin:top center; opacity:.82;
  box-shadow:var(--sh-mid); transform:translateZ(-46px) translateY(-30px) scale(.955);
  transition:transform .55s cubic-bezier(.22,.61,.36,1), opacity .55s}
.card--back{position:absolute; inset:0; z-index:1; transform-origin:top center; opacity:.5;
  box-shadow:var(--sh-back); transform:translateZ(-100px) translateY(-58px) scale(.908);
  transition:transform .55s cubic-bezier(.22,.61,.36,1), opacity .55s}

/* 4) HOVER = lift the front (deeper shadow) and fan the stack open */
.deck:hover .card--front{transform:translateZ(58px);  box-shadow:0 26px 24px rgba(26,22,30,.16),0 96px 92px -16px rgba(26,22,30,.3)}
.deck:hover .card--mid  {transform:translateZ(-46px)  translateY(-46px) scale(.955); opacity:.9}
.deck:hover .card--back {transform:translateZ(-100px) translateY(-88px) scale(.908); opacity:.62}

/* 5) A chip popped forward on its own Z plane for extra parallax */
.chip{position:absolute; right:-10px; bottom:30px; z-index:5; transform:translateZ(80px)}

@media (prefers-reduced-motion: reduce){
  .deck,.card--front,.card--mid,.card--back{transition:none}
}
```

## JavaScript

Optional. Tilts the `preserve-3d` deck toward the cursor so the layers separate in
real depth. rAF-throttled and **gated behind `prefers-reduced-motion`**.

```js
(function(){
  var stage = document.querySelector('.stage'),   // any element that bounds the pointer area
      deck  = document.querySelector('.deck');
  if(!stage || !deck) return;
  if(window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) return;
  var raf=0, ry=0, rx=0;
  function apply(){ raf=0; deck.style.transform='rotateX('+rx.toFixed(2)+'deg) rotateY('+ry.toFixed(2)+'deg)'; }
  stage.addEventListener('pointermove', function(e){
    var r=stage.getBoundingClientRect();
    ry =  ((e.clientX-r.left)/r.width  - .5) * 15;   // rotateY  (left/right)
    rx = -((e.clientY-r.top )/r.height - .5) * 11;   // rotateX  (up/down)
    if(!raf) raf=requestAnimationFrame(apply);
  });
  stage.addEventListener('pointerleave', function(){ ry=rx=0; deck.style.transform='rotateX(0deg) rotateY(0deg)'; });
})();
```

## How it works

- **Shared footprint:** the front card is in flow and defines the deck's size; the
  back plates are `position:absolute; inset:0`, so they're identical rectangles you
  then transform — the stack stays perfectly aligned at any width.
- **Recede with three knobs at once:** each layer back gets a negative `translateY`
  (peeks above the one in front), a smaller `scale`, and lower `opacity`. Doing all
  three together is what your eye reads as "further away".
- **`transform-origin: top center`** on the back plates means scaling shrinks them
  *downward/inward*, leaving the top edge exactly where `translateY` put it — so the
  peeking "tab" height is predictable instead of drifting.
- **Layered `box-shadow`** is the real depth cue: a stack of soft shadows (small→large
  blur, with a negative spread on the wide ones) mimics how light actually wraps a
  raised object far better than a single shadow. Front gets the deepest cast, back
  plates get progressively flatter ones.
- **`perspective` + `preserve-3d` + `translateZ`** put the layers on a genuine Z-axis,
  so the JS tilt (and the hover `translateZ` lift) move them through space rather than
  just sliding them on a flat plane.

## Customization

- **More layers:** add `.card--back2` etc., continuing the pattern — each step ~`translateY(-28px)`,
  `scale(-.045)`, `opacity(-.16)`, `translateZ(-54px)`. Keep the total to 3–4 or it turns to mush.
- **Light vs dark stage:** on a light surface (like this demo) dark shadows read crisply.
  On a dark stage, lean on a lighter 1px top border + a subtle inner highlight for lift,
  since a dark cast shadow barely shows.
- **Flat / no-3D variant:** drop `perspective`, `preserve-3d` and every `translateZ` —
  the offset + scale + opacity + layered shadow alone still gives a clean stacked look.
- **Spread direction:** offset on `translateX` (or both axes) instead of `translateY`
  to fan the deck sideways like a hand of cards.
- **Accent:** retint the shadow rgba slightly warm/cool to match the brand, and recolor
  the badge/eyebrow with one confident accent.

## Accessibility & performance

- Animate only **`transform` / `opacity` / `box-shadow`** — all compositor-friendly.
  rAF-throttle the pointer handler (as above) so you set the transform once per frame.
- Respect **`prefers-reduced-motion`**: skip attaching the pointer listener in JS *and*
  zero the CSS transitions; the static, nicely-stacked deck is the reduced-motion state.
- Big multi-stop shadows on many elements can cost paint time — keep the deepest shadow
  on the front card only and give the recessed plates cheaper, flatter shadows.
- Mark the purely-decorative back plates `aria-hidden="true"`; keep all real content,
  the heading and the link in the in-flow front card so the reading/tab order is sane.
- Ensure the front card's text contrast holds against its own background, independent of
  the layers behind it.

## Gotchas

- **`preserve-3d` doesn't chain through a flattening parent.** If a child needs its own
  `translateZ` to pop (the floating chip), make it a child of the `preserve-3d` deck —
  not nested inside a card that has the default `transform-style:flat`, which would
  flatten it back onto the card's plane.
- **`overflow:hidden` kills the 3D.** Don't put it on the deck or a card that holds
  Z-translated children; clip the image with its own inner wrapper instead.
- **Transform order matters:** write `translateZ() translateY() scale()` — reorder it and
  the peek offset and foreshortening fight each other.
- **Negative offsets can clip.** The back plates sit *above* the deck's box; give the
  surrounding stage enough top padding (and don't wrap it in `overflow:hidden`) or the
  peeking tabs get cut off.
- **One giant blurry shadow ≠ depth.** The realism is in *stacking several* low-alpha
  shadows; a single `0 40px 80px` reads as a flat grey halo.
