36 CSS Stacked Cards09 / 36

CSS + JSMIT licensed

Swipeable / Click-to-Front Stacked Cards

A deck where the front card is the interface: click it and it slides laterally out of bounds, drops to the lowest layer, and glides back into place at the rear — cycling the next card forward. A tactile way to browse a small, ordered set.

Published Updated

Live Demo
Try it

The code

<section class="scd-09" aria-label="Click-to-front card deck">
  <div class="scd-09__deck" id="scd-09-deck">
    <button class="scd-09__card" style="--c:oklch(0.63 0.17 20)"><span>Ticket #4821</span><b>Payment retry loop</b><i>High · Billing</i></button>
    <button class="scd-09__card" style="--c:oklch(0.66 0.15 150)"><span>Ticket #4822</span><b>Slow search on mobile</b><i>Medium · Web</i></button>
    <button class="scd-09__card" style="--c:oklch(0.64 0.16 250)"><span>Ticket #4823</span><b>Export to CSV missing</b><i>Low · Reports</i></button>
    <button class="scd-09__card" style="--c:oklch(0.64 0.16 300)"><span>Ticket #4824</span><b>SSO redirect fails</b><i>High · Auth</i></button>
  </div>
  <div class="scd-09__foot"><button class="scd-09__cyc" id="scd-09-next">Next ticket ↻</button><p id="scd-09-live" aria-live="polite" class="scd-09__sr"></p></div>
</section>
.scd-09,
.scd-09 *,
.scd-09 *::before,
.scd-09 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.scd-09 {
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 28px;
  padding: 50px 20px;
  background: #101219;
}

.scd-09__deck {
  position: relative;
  width: min(320px,86vw);
  height: 220px;
}

.scd-09__card {
  position: absolute;
  inset: 0;
  text-align: left;
  border: none;
  cursor: pointer;
  padding: 24px;
  border-radius: 20px;
  color: #fff;
  background: var(--c);
  box-shadow: 0 24px 46px -26px rgba(0,0,0,.8);
  display: flex;
  flex-direction: column;
  gap: 8px;
  transform: translateY(calc(var(--depth,0) * 16px)) scale(calc(1 - var(--depth,0) * .05));
  z-index: calc(10 - var(--depth,0));
  transition: transform .5s cubic-bezier(.3,.9,.3,1),opacity .5s;
}

.scd-09__card.scd-09__sending {
  transform: translateX(130%) rotate(12deg);
  opacity: 0;
  transition: transform .45s ease-in,opacity .45s;
}

.scd-09__card span {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .08em;
  opacity: .85;
}

.scd-09__card b {
  font-size: 20px;
  font-weight: 800;
}

.scd-09__card i {
  font-style: normal;
  font-size: 12px;
  opacity: .85;
  margin-top: auto;
}

.scd-09__card:focus-visible {
  outline: 3px solid #fff;
  outline-offset: 3px;
}

.scd-09__cyc {
  background: #232838;
  color: #fff;
  border: 1px solid #333a4e;
  padding: 11px 22px;
  border-radius: 12px;
  font-weight: 700;
  font-size: 14px;
  cursor: pointer;
  transition: background .2s;
}

.scd-09__cyc:hover {
  background: #2e3446;
}

.scd-09__cyc:focus-visible {
  outline: 3px solid oklch(0.66 0.15 250);
  outline-offset: 2px;
}

.scd-09__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
}

@media (prefers-reduced-motion: reduce) {
  .scd-09__card {
    transition: transform .2s,opacity .2s;
  }
}
(() => {
  const root = document.querySelector('.scd-09');
  const deck = root.querySelector('#scd-09-deck');
  const live = root.querySelector('#scd-09-live');
  const paint = () => [...deck.children].forEach((c,i) => c.style.setProperty('--depth', i));
  function cycle(){
    const front = deck.firstElementChild; if(!front) return;
    front.classList.add('scd-09__sending');
    front.addEventListener('transitionend', () => {
      front.classList.remove('scd-09__sending');
      deck.appendChild(front); paint();
      const nf = deck.firstElementChild.querySelector('b');
      if(nf) live.textContent = 'Now showing ' + nf.textContent;
    }, {once:true});
  }
  deck.addEventListener('click', e => { if(e.target.closest('.scd-09__card') === deck.firstElementChild) cycle(); });
  root.querySelector('#scd-09-next').addEventListener('click', cycle);
  paint();
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Stacked Card from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Swipeable / Click-to-Front Stacked Cards
Source: https://codefronts.com/components/css-stacked-cards/swipeable-click-to-front-stacked-cards/

A deck where the front card is the interface: click it and it slides laterally out of bounds, drops to the lowest layer, and glides back into place at the rear — cycling the next card forward. A tactile way to browse a small, ordered set.
## HTML
```html
<section class="scd-09" aria-label="Click-to-front card deck">
  <div class="scd-09__deck" id="scd-09-deck">
    <button class="scd-09__card" style="--c:oklch(0.63 0.17 20)"><span>Ticket #4821</span><b>Payment retry loop</b><i>High · Billing</i></button>
    <button class="scd-09__card" style="--c:oklch(0.66 0.15 150)"><span>Ticket #4822</span><b>Slow search on mobile</b><i>Medium · Web</i></button>
    <button class="scd-09__card" style="--c:oklch(0.64 0.16 250)"><span>Ticket #4823</span><b>Export to CSV missing</b><i>Low · Reports</i></button>
    <button class="scd-09__card" style="--c:oklch(0.64 0.16 300)"><span>Ticket #4824</span><b>SSO redirect fails</b><i>High · Auth</i></button>
  </div>
  <div class="scd-09__foot"><button class="scd-09__cyc" id="scd-09-next">Next ticket ↻</button><p id="scd-09-live" aria-live="polite" class="scd-09__sr"></p></div>
</section>
```
## CSS
```css
.scd-09,
.scd-09 *,
.scd-09 *::before,
.scd-09 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.scd-09 {
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 28px;
  padding: 50px 20px;
  background: #101219;
}

.scd-09__deck {
  position: relative;
  width: min(320px,86vw);
  height: 220px;
}

.scd-09__card {
  position: absolute;
  inset: 0;
  text-align: left;
  border: none;
  cursor: pointer;
  padding: 24px;
  border-radius: 20px;
  color: #fff;
  background: var(--c);
  box-shadow: 0 24px 46px -26px rgba(0,0,0,.8);
  display: flex;
  flex-direction: column;
  gap: 8px;
  transform: translateY(calc(var(--depth,0) * 16px)) scale(calc(1 - var(--depth,0) * .05));
  z-index: calc(10 - var(--depth,0));
  transition: transform .5s cubic-bezier(.3,.9,.3,1),opacity .5s;
}

.scd-09__card.scd-09__sending {
  transform: translateX(130%) rotate(12deg);
  opacity: 0;
  transition: transform .45s ease-in,opacity .45s;
}

.scd-09__card span {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .08em;
  opacity: .85;
}

.scd-09__card b {
  font-size: 20px;
  font-weight: 800;
}

.scd-09__card i {
  font-style: normal;
  font-size: 12px;
  opacity: .85;
  margin-top: auto;
}

.scd-09__card:focus-visible {
  outline: 3px solid #fff;
  outline-offset: 3px;
}

.scd-09__cyc {
  background: #232838;
  color: #fff;
  border: 1px solid #333a4e;
  padding: 11px 22px;
  border-radius: 12px;
  font-weight: 700;
  font-size: 14px;
  cursor: pointer;
  transition: background .2s;
}

.scd-09__cyc:hover {
  background: #2e3446;
}

.scd-09__cyc:focus-visible {
  outline: 3px solid oklch(0.66 0.15 250);
  outline-offset: 2px;
}

.scd-09__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
}

@media (prefers-reduced-motion: reduce) {
  .scd-09__card {
    transition: transform .2s,opacity .2s;
  }
}
```

## JavaScript
```js
(() => {
  const root = document.querySelector('.scd-09');
  const deck = root.querySelector('#scd-09-deck');
  const live = root.querySelector('#scd-09-live');
  const paint = () => [...deck.children].forEach((c,i) => c.style.setProperty('--depth', i));
  function cycle(){
    const front = deck.firstElementChild; if(!front) return;
    front.classList.add('scd-09__sending');
    front.addEventListener('transitionend', () => {
      front.classList.remove('scd-09__sending');
      deck.appendChild(front); paint();
      const nf = deck.firstElementChild.querySelector('b');
      if(nf) live.textContent = 'Now showing ' + nf.textContent;
    }, {once:true});
  }
  deck.addEventListener('click', e => { if(e.target.closest('.scd-09__card') === deck.firstElementChild) cycle(); });
  root.querySelector('#scd-09-next').addEventListener('click', cycle);
  paint();
})();
```

How this works

Cards are absolutely stacked; their depth is data, not markup order. A tiny controller keeps an array of card elements and, on click of the front card (or the Next button), applies a .sending class that flings it sideways, then re-appends it to the end and re-paints every card's --depth custom property. CSS maps --depth to translateY+scale+z-index, so the reshuffle animates cleanly.

Buttons and Enter/Space on the focused front card trigger the same cycle, and aria-live reads the new front card's title.

Make it yours

  • Change the fling direction/curve in the .sending transform.
  • Set stack depth via the --depth → transform mapping.
  • Add a Prev action by re-inserting the last card at the front.
  • Recolour cards through their inline accent.

Gotchas — read before shipping

  • Drive depth from a data attribute, not DOM order, so re-appending doesn't fight your CSS.
  • Wait for transitionend before re-appending, or the card snaps.
  • Keep a button + key handler so it's not click-only (a11y).

Browser support

ChromeSafariFirefoxEdge
111+15.4+113+111+

Floor set by oklch() as this demo is written. The core technique itself goes back further — Chrome 49+.

CSS custom properties + transforms + a few lines of vanilla JS; supported across modern browsers.

Techniques used in this demo

Search CodeFronts

Loading…