Bento Card Stack
Bento grid with depth-offset card stacks — on hover, each stack fans out via a CSS spring transition, no JavaScript required.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Cards has 41 effects, including 6 free. Explore the category →
3 usage examples



How it works
The structure relies on a 4×3 CSS Grid (repeat(4,1fr) columns, repeat(3,1fr) rows). Two large cells span 2/span 2 for the main stacks; two bottom cells span 2 columns for the mini-cards — all inside .bento-container with max-width: 600px and aspect-ratio: 4/3.
The stacking effect is pure CSS: three .stacked-card are position: absolute, centered via translate(-50%, -50%). The second card is offset by translateY(8px) scale(0.95) and the third by translateY(16px) scale(0.90) — no CSS 3D or perspective, depth is simulated solely through Y offset and scale reduction.
On :hover over a bento cell, three simultaneous transitions 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) (spring with slight overshoot) play: the front card moves to translateX(-30%) rotate(-10deg), the middle card rises 5 px, the back card moves to translateX(30%) rotate(10deg) — forming a symmetrical fan. The .mini-card elements in the bottom cells follow the same logic with translateY and a light rotation.
The JS exposes replayAnimation() to replay the .bento-animated variant (entry via @keyframes bentoFadeIn) — it forces a reflow via offsetHeight then resets the CSS animation. The card-stack mechanism itself is 100% CSS with no JS initialization needed.
Accessibility
- prefers-reduced-motion not implemented: the code contains no
prefers-reduced-motionmedia query. CSS transitions run even when the user has requested reduced motion. Minimal fix:@media (prefers-reduced-motion: reduce) { .stacked-card, .mini-card { transition: none; } }. - The animation is triggered by CSS :hover only — no keyboard equivalent (
:focus-withinabsent). Keyboard users do not see the fan-out. - No ARIA semantics on the cells:
.bento-cellare non-interactive<div>elements with noroleoraria-label. Addrole="group"andaria-labelon each stack if the card content is meaningful. - The card color gradients (close indigo-violet shades) provide limited contrast for white text overlaid on top — verify with your custom palette using a WCAG contrast tool.
.mini-cardelements are purely decorative<div>with no text content: no text alternative is required as long as they remain visual.
Browser compatibility
Requires CSS Grid and CSS transitions — supported in all modern browsers since 2017. Zero external dependencies.
On touch screens, CSS :hover does not fire: cards stay stacked in their rest state, still readable. No JS errors.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="bento-container">
<div class="bento-grid bento-cardstack">
<!-- Wide cell 1: 3-card stack -->
<div class="bento-cell cell-1">
<div class="card-stack">
<div class="stacked-card"><span>Card 3</span></div>
<div class="stacked-card"><span>Card 2</span></div>
<div class="stacked-card"><span>Card 1</span></div>
</div>
</div>
<!-- Wide cell 2: 3-card stack -->
<div class="bento-cell cell-2">
<div class="card-stack">
<div class="stacked-card"><span>C</span></div>
<div class="stacked-card"><span>B</span></div>
<div class="stacked-card"><span>A</span></div>
</div>
</div>
<!-- Bottom-left cell: mini-cards -->
<div class="bento-cell cell-3">
<div class="mini-cards">
<div class="mini-card"></div>
<div class="mini-card"></div>
<div class="mini-card"></div>
</div>
</div>
<!-- Bottom-right cell: mini-cards -->
<div class="bento-cell cell-4">
<div class="mini-cards">
<div class="mini-card"></div>
<div class="mini-card"></div>
<div class="mini-card"></div>
</div>
</div>
</div>
</div>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| --bg-elevated | #1a1a24 | Background of bento cells containing the stacks — change for stronger contrast or a brand tint. |
| --bg-card | #12121a | Base background of bento cells without the elevated variant. |
| --border | rgba(255,255,255,0.08) | Border of stacked cards. Raise opacity to 0.18 for more visible outlines. |
| --radius-md | 0.5rem | Corner radius of .stacked-card elements. Set to 0 for square cards or 1rem for more roundness. |
| .stacked-card:nth-child(n) background | indigo→violet gradients | Replace the linear-gradient on each :nth-child(1/2/3) to customize the card palette one by one. |
| translateX(±30%) in :hover | ±30% | Lateral spread on hover. ±45% for a wide fan, ±15% for a subtle spread. |
| rotate(±10deg) in :hover | ±10deg | Rotation angle of side cards on hover. 0deg = spread without rotation, 18deg = pronounced fan. |
| transition on .stacked-card | 0.4s cubic-bezier(.175,.885,.32,1.275) | Duration and easing of the spring animation. Use 0.25s ease-out for a subdued feel or 0.6s for a more elastic bounce. |
FAQ
<div class="stacked-card"> as the first child (it will be the deepest visually). Then add its rest rule: .bento-cardstack .stacked-card:nth-child(4) { transform: translate(-50%,-50%) translateY(24px) scale(0.85); z-index: 0; } and the matching hover rule with a wider translateX.:hover does not fire on touch. To enable it on tap, add a touchstart listener that toggles an .is-open class on the cell, then target .bento-cell.is-open .stacked-card in your CSS fan-out rules instead of :hover.translate(-50%,-50%) translateY(-35%) rotate(-8deg); middle card → translate(-50%,-65%); back card → translate(-50%,-50%) translateY(35%) rotate(8deg). The cubic-bezier and duration stay unchanged.