Pure CSS Cards & galleries #loop

Opening book in pure CSS

Cover and pages hinge on the spine, each opening to a slightly different angle.

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

How it works

  1. Cover and pages are stacked sheets, all with transform-origin: left — the spine is the hinge.
  2. Each sheet gets its index in --n. A 1px translateZ per sheet stops them z-fighting.
  3. The keyframe’s end angle is var(--end), computed per sheet, so one animation fans the pages out.
  4. A small per-sheet animation-delay makes the cover lead and the pages follow.

Key techniques

  • transform-origin: left
  • var() end angle in @keyframes
  • staggered delays

Copy-paste code

HTML 9 lines

<div class="scene">
  <div class="book">
    <i style="--n:0"></i>
    <i style="--n:1"></i>
    <i style="--n:2"></i>
    <i style="--n:3"></i>
    <i style="--n:4" class="cover"></i>
  </div>
</div>

CSS 36 lines

.scene {
  perspective: 1000px;
}

.book {
  position: relative;
  width: 130px;
  height: 175px;
  border-radius: 2px 8px 8px 2px;
  background: #4a3a99;               /* back cover */
  transform-style: preserve-3d;
  transform: translateX(60px) rotateX(24deg) rotateY(-12deg);
}

.book i {
  --end: calc(-118deg - var(--n) * 12deg);
  position: absolute;
  inset: 3px 3px 3px 0;
  border-radius: 0 5px 5px 0;
  background: linear-gradient(90deg, #cfd3e6, #fff 14%);
  transform-origin: left center;
  transform: translateZ(calc(var(--n) * 1px));
  animation: open 3.6s ease-in-out infinite alternate;
  animation-delay: calc((4 - var(--n)) * 0.14s);
}

.book .cover {
  inset: 0;
  border-radius: 2px 8px 8px 2px;
  background: linear-gradient(90deg, #4a3a99, #8b6cff 12%);
}

@keyframes open {
  0%, 10%   { transform: translateZ(calc(var(--n) * 1px)) rotateY(0deg); }
  90%, 100% { transform: translateZ(calc(var(--n) * 1px)) rotateY(var(--end)); }
}

Sass source 54 lines

.d-book {
  position: relative;
  width: 104px;
  height: 140px;
  transform-style: preserve-3d;
  transform: translateX(46px) rotateX(24deg) rotateY(-12deg);
  // back cover
  background: color-mix(in srgb, var(--accent) 70%, #000);
  border-radius: 2px 8px 8px 2px;

  // Pages then cover (last <i>), all hinged on the spine. --n is 0…4 from the markup.
  i {
    position: absolute;
    inset: 3px 3px 3px 0;
    border-radius: 0 5px 5px 0;
    background: linear-gradient(90deg, #cfd3e6, #fff 14%);
    transform-origin: left center;
    // Later sheets sit higher and open further.
    --end: calc(-118deg - var(--n) * 12deg);
    transform: translateZ(calc(var(--n) * 1px));
    animation: d-book-open 3.6s ease-in-out infinite alternate;
    animation-delay: calc((4 - var(--n)) * 0.14s);
  }

  i:last-of-type {
    inset: 0;
    border-radius: 2px 8px 8px 2px;
    background: linear-gradient(90deg, color-mix(in srgb, var(--accent) 60%, #000), var(--accent) 12%);
  }

  // title, stamped on the inside back so it shows once the book is open
  b {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    color: #fff;
    font-size: 20px;
    line-height: 1.1;
    text-align: center;
  }
}

@keyframes d-book-open {
  0%,
  10% {
    transform: translateZ(calc(var(--n) * 1px)) rotateY(0deg);
  }

  90%,
  100% {
    transform: translateZ(calc(var(--n) * 1px)) rotateY(var(--end));
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.