Pure CSS Cards & galleries #loop

Card fan in pure CSS

Five cards share one pivot below the hand and fan out by index.

  • 31 lines of CSS
  • 9 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed

How it works

  1. All five cards sit exactly on top of each other.
  2. transform-origin: 50% 170% moves the pivot far below the card, so rotateZ swings it along an arc instead of spinning it in place.
  3. The index runs −2…2, so rotateZ(calc(var(--i) * 16deg)) fans symmetrically around an upright middle card.
  4. A few px of translateZ per card gives each its own depth, which avoids flicker where they overlap.

Key techniques

  • transform-origin far below the card
  • rotateZ(var(--i) × step)
  • translateZ per card to avoid z-fighting

Copy-paste code

HTML 9 lines

<div class="scene">
  <div class="hand">
    <i style="--i:-2">A<small></small></i>
    <i style="--i:-1">K<small></small></i>
    <i style="--i:0">Q<small></small></i>
    <i style="--i:1">J<small></small></i>
    <i style="--i:2">10<small></small></i>
  </div>
</div>

CSS 31 lines

.scene {
  perspective: 800px;
}

.hand {
  position: relative;
  width: 110px;
  height: 156px;
  transform-style: preserve-3d;
  transform: rotateX(26deg);
}

.hand i {
  position: absolute;
  inset: 0;
  padding: 8px 12px;
  border-radius: 10px;
  background: linear-gradient(160deg, #fff, #d9dcec);
  color: #14172b;
  font: 800 1.6rem/1 system-ui;
  box-shadow: 0 8px 16px -8px #000;
  transform-origin: 50% 170%;
  animation: fan 3s ease-in-out infinite alternate;
}

.hand small { display: block; font-size: 1.2rem; }

@keyframes fan {
  0%, 15%   { transform: translateZ(calc(var(--i) * 1px)) rotateZ(0deg); }
  85%, 100% { transform: translateZ(calc(var(--i) * 6px)) rotateZ(calc(var(--i) * 16deg)); }
}

Sass source 42 lines

.d-cardfan {
  position: relative;
  width: 84px;
  height: 120px;
  transform-style: preserve-3d;
  transform: translateY(10px) rotateX(26deg);

  // --i runs -2…2 from the markup, so the middle card stays upright.
  i {
    position: absolute;
    inset: 0;
    padding: 6px 9px;
    border-radius: 9px;
    background: linear-gradient(160deg, #fff, #d9dcec);
    color: #14172b;
    font-size: 20px;
    font-style: normal;
    font-weight: 800;
    line-height: 1;
    box-shadow: 0 8px 16px -8px #000;
    // The pivot sits well below the card, like the wrist holding the hand.
    transform-origin: 50% 170%;
    animation: d-cardfan 3s ease-in-out infinite alternate;

    small {
      display: block;
      font-size: 16px;
    }
  }
}

@keyframes d-cardfan {
  0%,
  15% {
    transform: translateZ(calc(var(--i) * 1px)) rotateZ(0deg);
  }

  85%,
  100% {
    transform: translateZ(calc(var(--i) * 6px)) rotateZ(calc(var(--i) * 16deg));
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.