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.

Category3D
TypeWeb Component (<mv-card-stack>)
Statusbeta
Keywordscard-stack, swipe, deck, pile, fling, drag, testimonials, stack

When to use

Avoid when

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

NameTypeDefaultDescription
labelstringAccessible name of the region.
accept-label / reject-labelstringLike / SkipText for the stamps, buttons and announcements.
no-loopbooleanSwiped cards leave the stack (data-empty + mv-empty at the end) instead of going back underneath.
no-controlsbooleanHides 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.
thresholdnumber0.3Fraction of the width to cross to confirm a slow drag (a quick flick is enough).

Properties

NameTypeDescription
currentElement | nullTop card.
remainingnumberCards still in the stack.

Methods

NameDescription
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

NameDescription
mv-swipedetail: { direction: 'left' | 'right' | 'next', index, card }.
mv-emptyNo cards left (no-loop).

Content structure

NameDescription
childrenEach child is a card (role=group “card”, named after its title).

CSS classes

NameDescription
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

NameDefaultDescription
--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.