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.
Canonical markup, to start from and customize with attributes, data-* and CSS variables:
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(20rem,100%),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, Harbor & Pine</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
label
string
Accessible name of the region.
accept-label / reject-label
string
Like / Skip
Text for the stamps, buttons and announcements.
no-loop
boolean
Swiped cards leave the stack (data-empty + mv-empty at the end) instead of going back underneath.
no-controls
boolean
Hides the Skip / Like buttons.
autoplay
number (ms)
Advances to the next card on an interval (4000 ms if empty); pauses on hover and focus, disabled under reduced motion.
threshold
number
0.3
Fraction of the width to cross to confirm a slow drag (a quick flick is enough).
Properties
Name
Type
Description
current
Element | null
Top card.
remaining
number
Cards still in the stack.
strings
Partial<Record<string, string>>
Overrides for the default text (en-US): roleDescription (aria-roledescription of the stack), card (aria-roledescription of each card), cardLabel (card name when it has no aria-label, {name}, {index} and {total}), hint (hidden keyboard instructions, {reject} and {accept}), undo (Undo button), next (verdict word for next()), announceVerdict ({verdict} and {name}, last card), announceVerdictNext (adds {next}, the new top card), announceUndo ({name}). App-wide translations: setStrings() from core/i18n.js, under the key "card-stack". Can be set before the element is defined.
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.
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; autoplay ticks are not announced, and autoplay pauses on hover and focus. The focusable deck is a group named by label. Named Skip / Undo / Like buttons; Undo is disabled until something was swiped. Reduced motion: instant flings, tucks and settles, no autoplay; dragging still works.