beta
Card Stack — <mv-card-stack>
Swipeable card pile: cards rest at their own slight tilt and straighten as the top one is pulled away; the top card pivots around the point you grabbed, a verdict badge and edge tint grow with the pull while the matching button fills, flings keep the gesture’s momentum, and next() tucks the card under the pile; buttons, arrow keys, undo, loop or run-out, and autoplay.
Category 3D Type Web Component (<mv-card-stack>) Status beta Keywords card-stack, swipe, deck, pile, fling, drag, testimonials, stack
When to use
Users must accept or skip items one at a time, as in matching, curation or onboarding choices Testimonials or highlights should cycle as a tactile, slightly tilted pile with autoplay Swipe decisions need undo, arrow-key control and announced verdicts
Avoid when
Items should be browsed or compared freely rather than decided on one by one → use Carousel instead
Get Card Stack. Included in every plan: the Pro pack ships all 226 components with the CLI and the MCP server used below.
See plans
Install
node scripts/add.mjs card-stack --out ./src/marvelous
AI agent with the Marvelous UI MCP server: install_components({ slugs: ["card-stack"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, core/pointer.js, components/card-stack/card-stack.js, components/card-stack/card-stack.css.
Usage
Canonical markup — start from it and customize with attributes, data-* and CSS variables:
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(20rem,1fr));gap:3rem;align-items:start;width:100%;max-width:52rem">
<div style="display:grid;gap:1rem;justify-items:center">
<mv-card-stack id="cs-demo" label="Weekend ideas" accept-label="I’m in" reject-label="Skip">
<article>
<div aria-hidden="true" style="aspect-ratio:4/3;background:radial-gradient(circle at 30% 24%,#ecfeff 0 8%,transparent 8.5%),linear-gradient(150deg,transparent 57%,#0f766e 57.3%),linear-gradient(200deg,transparent 67%,#134e4a 67.3%),linear-gradient(180deg,#0ea5e9,#67e8f9 62%,#ecfeff)"></div>
<div style="padding:1rem 1.1rem 1.2rem;display:grid;gap:.35rem">
<h4 style="margin:0;font:700 1.15rem/1.2 var(--mv-font-sans);letter-spacing:-.01em">Banff</h4>
<p style="margin:0;font-size:.85rem;color:var(--mv-fg-muted)">Canada · 2 days · from $180</p>
<p style="margin:.35rem 0 0;display:flex;flex-wrap:wrap;gap:.35rem"><span style="padding:.15rem .55rem;border-radius:999px;background:var(--mv-bg-muted);font-size:.75rem">Kayaking</span><span style="padding:.15rem .55rem;border-radius:999px;background:var(--mv-bg-muted);font-size:.75rem">Hiking</span><span style="padding:.15rem .55rem;border-radius:999px;background:var(--mv-bg-muted);font-size:.75rem">Camping</span></p>
</div>
</article>
<article>
<div aria-hidden="true" style="aspect-ratio:4/3;background:radial-gradient(circle at 41% 30%,#f8fafc 0 8%,transparent 8.5%),linear-gradient(157deg,transparent 57%,#e2e8f0 57.3%),linear-gradient(194deg,transparent 67%,#475569 67.3%),linear-gradient(180deg,#64748b,#bae6fd 62%,#f8fafc)"></div>
<div style="padding:1rem 1.1rem 1.2rem;display:grid;gap:.35rem">
<h4 style="margin:0;font:700 1.15rem/1.2 var(--mv-font-sans);letter-spacing:-.01em">Cliffs of Moher</h4>
<p style="margin:0;font-size:.85rem;color:var(--mv-fg-muted)">Ireland · 1 day · from $65</p>
<p style="margin:.35rem 0 0;display:flex;flex-wrap:wrap;gap:.35rem"><span style="padding:.15rem .55rem;border-radius:999px;background:var(--mv-bg-muted);font-size:.75rem">Cliffs</span><span style="padding:.15rem .55rem;border-radius:999px;background:var(--mv-bg-muted);font-size:.75rem">Cycling</span><span style="padding:.15rem .55rem;border-radius:999px;background:var(--mv-bg-muted);font-size:.75rem">Seafood</span></p>
</div>
</article>
<article>
<div aria-hidden="true" style="aspect-ratio:4/3;background:radial-gradient(circle at 52% 36%,#fdba74 0 8%,transparent 8.5%),linear-gradient(164deg,transparent 57%,#4c1d95 57.3%),linear-gradient(188deg,transparent 67%,#1e1b4b 67.3%),linear-gradient(180deg,#6d28d9,#f472b6 62%,#fdba74)"></div>
<div style="padding:1rem 1.1rem 1.2rem;display:grid;gap:.35rem">
<h4 style="margin:0;font:700 1.15rem/1.2 var(--mv-font-sans);letter-spacing:-.01em">Lake Como</h4>
<p style="margin:0;font-size:.85rem;color:var(--mv-fg-muted)">Italy · 3 days · from $240</p>
<p style="margin:.35rem 0 0;display:flex;flex-wrap:wrap;gap:.35rem"><span style="padding:.15rem .55rem;border-radius:999px;background:var(--mv-bg-muted);font-size:.75rem">Lake</span><span style="padding:.15rem .55rem;border-radius:999px;background:var(--mv-bg-muted);font-size:.75rem">Paddleboarding</span><span style="padding:.15rem .55rem;border-radius:999px;background:var(--mv-bg-muted);font-size:.75rem">Gelato</span></p>
</div>
</article>
<article>
<div aria-hidden="true" style="aspect-ratio:4/3;background:radial-gradient(circle at 63% 24%,#fff7ed 0 8%,transparent 8.5%),linear-gradient(171deg,transparent 57%,#7c2d12 57.3%),linear-gradient(182deg,transparent 67%,#431407 67.3%),linear-gradient(180deg,#b45309,#fcd34d 62%,#fff7ed)"></div>
<div style="padding:1rem 1.1rem 1.2rem;display:grid;gap:.35rem">
<h4 style="margin:0;font:700 1.15rem/1.2 var(--mv-font-sans);letter-spacing:-.01em">Napa Valley</h4>
<p style="margin:0;font-size:.85rem;color:var(--mv-fg-muted)">California · 2 days · from $210</p>
<p style="margin:.35rem 0 0;display:flex;flex-wrap:wrap;gap:.35rem"><span style="padding:.15rem .55rem;border-radius:999px;background:var(--mv-bg-muted);font-size:.75rem">Vineyards</span><span style="padding:.15rem .55rem;border-radius:999px;background:var(--mv-bg-muted);font-size:.75rem">Wine tasting</span></p>
</div>
</article>
<article>
<div aria-hidden="true" style="aspect-ratio:4/3;background:radial-gradient(circle at 74% 30%,#dbeafe 0 8%,transparent 8.5%),linear-gradient(178deg,transparent 57%,#1e3a8a 57.3%),linear-gradient(176deg,transparent 67%,#172554 67.3%),linear-gradient(180deg,#1d4ed8,#60a5fa 62%,#dbeafe)"></div>
<div style="padding:1rem 1.1rem 1.2rem;display:grid;gap:.35rem">
<h4 style="margin:0;font:700 1.15rem/1.2 var(--mv-font-sans);letter-spacing:-.01em">Algarve Coast</h4>
<p style="margin:0;font-size:.85rem;color:var(--mv-fg-muted)">Portugal · 3 days · from $150</p>
<p style="margin:.35rem 0 0;display:flex;flex-wrap:wrap;gap:.35rem"><span style="padding:.15rem .55rem;border-radius:999px;background:var(--mv-bg-muted);font-size:.75rem">Surfing</span><span style="padding:.15rem .55rem;border-radius:999px;background:var(--mv-bg-muted);font-size:.75rem">Coastal trails</span></p>
</div>
</article>
</mv-card-stack>
<p id="cs-demo-out" style="margin:0;min-height:1.3em;font-size:.85rem;color:var(--mv-fg-muted)">Swipe the card, or use ←/→ on your keyboard (Backspace to undo).</p>
</div>
<div style="display:grid;gap:1rem;justify-items:center;align-self:center">
<mv-card-stack label="Testimonials" autoplay="3800" no-controls style="--mv-card-stack-width:22rem;--mv-card-stack-offset:12px">
<figure style="margin:0;padding:1.3rem 1.4rem;display:grid;gap:1rem;min-height:11rem;align-content:space-between">
<blockquote style="margin:0;font-size:1rem;line-height:1.5">“We cut the time it takes to lay out our campaigns in half.”</blockquote>
<figcaption style="display:flex;gap:.7rem;align-items:center"><span aria-hidden="true" style="width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,#f472b6,#8b5cf6)"></span><span><strong style="display:block;font-size:.9rem">Maya Chen</strong><small style="color:var(--mv-fg-muted)">Art Director, Northwind Studio</small></span></figcaption>
</figure>
<figure style="margin:0;padding:1.3rem 1.4rem;display:grid;gap:1rem;min-height:11rem;align-content:space-between">
<blockquote style="margin:0;font-size:1rem;line-height:1.5">“The components hold up in production, even in Safari.”</blockquote>
<figcaption style="display:flex;gap:.7rem;align-items:center"><span aria-hidden="true" style="width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,#34d399,#0ea5e9)"></span><span><strong style="display:block;font-size:.9rem">Daniel Okafor</strong><small style="color:var(--mv-fg-muted)">Lead front-end, Rails & Co</small></span></figcaption>
</figure>
<figure style="margin:0;padding:1.3rem 1.4rem;display:grid;gap:1rem;min-height:11rem;align-content:space-between">
<blockquote style="margin:0;font-size:1rem;line-height:1.5">“Finally, animations that respect reduced motion.”</blockquote>
<figcaption style="display:flex;gap:.7rem;align-items:center"><span aria-hidden="true" style="width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,#fbbf24,#f97316)"></span><span><strong style="display:block;font-size:.9rem">Priya Raman</strong><small style="color:var(--mv-fg-muted)">Accessibility specialist</small></span></figcaption>
</figure>
</mv-card-stack>
<p style="margin:0;font-size:.85rem;color:var(--mv-fg-muted)">autoplay · no-controls · pauses on hover</p>
</div>
</div>
<script type="module">
const stack = document.getElementById("cs-demo");
const out = document.getElementById("cs-demo-out");
const saved = [];
stack.addEventListener("mv-swipe", (e) => {
const name = e.detail.card.querySelector("h4").textContent;
if (e.detail.direction === "right") saved.push(name);
out.textContent = e.detail.direction === "right" ? `Saved to your wishlist: ${saved.join(", ")}` : `Skipped ${name}`;
});
</script>
API
Attributes
Name Type Default Description labelstring Accessible name of the region. accept-label / reject-labelstring Like / SkipText for the stamps, buttons and announcements. no-loopboolean Swiped cards leave the stack (data-empty + mv-empty at the end) instead of going back underneath. no-controlsboolean Hides the Skip / Like buttons. autoplaynumber (ms) Advances to the next card on an interval (4000 ms if empty); pauses on hover and focus, disabled under reduced motion. thresholdnumber 0.3Fraction of the width to cross to confirm a slow drag (a quick flick is enough).
Properties
Name Type Description currentElement | null Top card. remainingnumber Cards still in the stack.
Methods
Name Description accept() / reject()Flings the top card right or left. next()Tucks the top card under the pile with no verdict (used by autoplay). undo()Brings back the last swiped card from the side it left. reset()Puts every card back in its original order.
Events
Name Description mv-swipedetail: { direction: 'left' | 'right' | 'next', index, card }. mv-emptyNo cards left (no-loop).
Content structure
Name Description childrenEach child is a card (role=group “card”, named after its title).
CSS classes
Name Description mv-card-stack-deck / -card / -tint / -stamp (-stamp-icon, -stamp-text) / -controls / -buttonGenerated parts; data-top on the top card, data-gone on cards that left the stack (no-loop), data-kind on buttons (reject | undo | accept).
CSS variables
Name Default Description --mv-card-stack-width20remCard width. --mv-card-stack-offset14pxHow far the pile settles below the top card. --mv-card-stack-tilt1Multiplier for the resting tilt of the cards below (0 = squared-up pile). --mv-card-stack-radiusvar(--mv-radius-2xl)Card radius. --mv-card-stack-accept / --mv-card-stack-rejectsuccess / dangerBadge, tint and button colors.
Accessibility
Region with aria-roledescription “card stack”. The stack is focusable: ← skip, → accept, Backspace or Ctrl/Cmd+Z undo (instructions exposed via aria-describedby). Only the top card is exposed; the others are aria-hidden and inert. Every verdict is announced (“I’m in: Lake Como. Next card: …”) in a polite live region. Named Skip / Undo / Like buttons; Undo is disabled until something was swiped. Reduced motion: instant flings, tucks and settles, no autoplay; dragging still works.