3D Menu Stack
Four menu links stacked in a 600 px perspective parent, each a little further back and paler than the one before (0, −10, −20, −30 px; opacity 1 down to 0.7); the item that is hovered, tapped or reached with the keyboard moves 20 px forward, slides 10 px right, turns −5° and takes an indigo-violet gradient with a drop shadow, in 0.3 s — a list whose depth tells the order, in CSS only.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. 3D has 50 effects, including 8 free. Explore the category →
3 usage examples



How it works
The root .fx-0009-root (#0a0a0f background, at least 300 px tall) defines three variables: --bg-card: #12121a, --border and --radius-md. It centers .menu-stack, a flex column (8 px gap) that carries perspective: 600px, and four links <a class="menu-stack-item">: block, bold 14 px white text, 12 px × 30 px padding, card background, thin border, 0.3 s transition on everything.
The depth is written item by item with :nth-child: the 1st stays at translateZ(0), the 2nd moves back 10 px at ×0.98 and 90% opacity, the 3rd to −20 px, ×0.96, 80%, the 4th to −30 px, ×0.94, 70%. The perspective adds to the scale: measured in Chromium and Firefox, the four items are 143, 138, 133 and 128 px wide, and the stack narrows toward the bottom like stairs seen from the front. On hover (or :focus-visible), the item takes translateZ(20px) translateX(10px) rotateY(-5deg), a #6366f1 → #8b5cf6 gradient, a shadow cast down and to the left, and z-index: 10. This transform replaces the one from the :nth-child line: the further back the item, the bigger its jump (20 px for the 1st, 50 px for the 4th). Measured: 4% of pixels change when hovering the 2nd item.
Two writing choices. The items are links: Tab reaches them, Enter follows them, and a real menu needs nothing more. The hover gradient sits on top of the card color (linear-gradient(…) var(--bg-card)): a gradient alone resets the background color to transparent, and the item losing hover would go through transparency for 0.3 s, which would make it fade on a light page. The items set back have an opacity below 1: that is intended (they fade with distance), but anything placed on them fades with them.
Accessibility
- Reduced motion: under
prefers-reduced-motion: reduce, a block scoped to.fx-0009-rootkeeps the transition on background and shadow only, and brings the chosen item totranslateZ(0): it comes to the front at once, without sliding or turning; the gradient and shadow mark it. Measured in Chromium and Firefox: no position transition running, 3.5% of pixels change on hover (4% without the preference). - Keyboard: four links, reached with Tab; focus gives exactly the hover state (
:focus-visible), plus the browser's focus ring. In Safari, Tab only stops on links when the "Press Tab to highlight each item" option is on (otherwise Option + Tab) — a browser setting, measured in WebKit. - Contrast: white text on
#12121aat 18.6:1; the 4th item, at 70% opacity, keeps 9.4:1 on the original background. The hovered item drops to 4.47:1 on the indigo and 4.2:1 on the violet#8b5cf6, under the 4.5:1 threshold for bold 14 px text: darken the end of the gradient (#7c3aedgives 5.7:1). - Touch: with a finger, the tapped item takes the hover state (measured with Chromium touch emulation and in Firefox), then the link is followed. Put a real address in each
href.
Browser compatibility
CSS only (perspective on the parent, transform on each item, :nth-child, :focus-visible), no dependencies. Measured in Chromium 151 and Firefox 153: same widths (143 down to 128 px), same hover, same reduced-motion behavior, 0 errors. Each item is transformed on its own inside its parent's perspective, with no nested 3D planes: Playwright's WebKit (26.5), which flattens preserve-3d scenes, renders this one like Chromium (2% difference, from text smoothing).
Without :focus-visible (old browser), keyboard focus does not bring the item forward: the browser's ring remains. Without perspective, the items no longer move back but keep their scale-down (×0.98 to ×0.94) and their paleness.
The code
HTML structure to paste into your page (the CSS + JS come with your purchase):
<div class="fx-0009-root">
<div class="demo-3d">
<div class="menu-stack"> <!-- perspective: 600px -->
<a class="menu-stack-item" href="#">Menu Item 1</a>
<a class="menu-stack-item" href="#">Menu Item 2</a> <!-- −10 px, ×0.98 -->
<a class="menu-stack-item" href="#">Menu Item 3</a> <!-- −20 px, ×0.96 -->
<a class="menu-stack-item" href="#">Menu Item 4</a> <!-- −30 px, ×0.94 -->
</div>
</div>
</div>
One-time payment, no subscription — the code is yours forever
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Item setback (CSS — translateZ of the four :nth-child) |
0, −10, −20, −30 px | Bigger steps (0, −25, −50, −75 px) deepen the stack and narrow the bottom further; beyond −100 px the last items get small. A 5th item needs its own :nth-child(5) line. |
Paleness (CSS — opacity of the :nth-child) |
1; 0.9; 0.8; 0.7 | Below 0.6 the last item's text gets hard to read on a light background. Set 1 everywhere for a stack that recedes without fading. |
Hover motion (CSS — transform of :hover, :focus-visible) |
translateZ(20px) translateX(10px) rotateY(-5deg) | rotateY(-10deg) stresses the opening-door feel; remove translateX for an item that moves forward without sliding. |
Colors (CSS — --bg-card, --border and hover gradient) |
#12121a; rgba(255, 255, 255, .08); #6366f1 → #8b5cf6 | Keep var(--bg-card) after the gradient: it is the color underneath, the one that avoids going through transparency when hover ends. |
Perspective (CSS — .menu-stack) |
600 px | 300 px exaggerates the setback and the turn; 1,200 px nearly flattens them. |
Duration (CSS — transition: .3s) |
0.3 s | The same duration is used both ways; 0.2 s gives a snappier menu. |
FAQ
translateZ(-30px) scale(.94) to translateZ(20px) with no scale, a 50 px move forward, while the 1st only moves 20 px. That is what makes a distant item "rise". For equal jumps, add each item's setback into its hover rule (for instance an extra translateZ(-10px) for the 2nd).<a> links, reachable with the keyboard, with the same relief on focus as on hover. Replace the href="#" with your addresses and, for main navigation, wrap the stack in a <nav aria-label="…">. Keep four to six items: beyond that, the last ones recede so much they get small.opacity: 1 everywhere and let the setback and scale-down tell the depth.