Pure CSS Cards & galleries #loop

Opening book in pure CSS

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

  • 55 lines of CSS
  • 9 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

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. One unit of translateZ per sheet stops them z-fighting, and never less than 1px (max(1px, var(--u))): on a small card one unit is under a pixel.
  3. Each sheet’s end angle is var(--end), computed per sheet, so the pages fan out.
  4. One number opens the book: --open, registered with @property so it can be animated, runs 0 → 1 and back. Each sheet turns over its own slice of it, the cover’s first and the bottom page’s last. Played backwards the slices come in the opposite order by themselves, so the book shuts bottom page first and the cover comes down last, on top. (A per-sheet animation-delay cannot do this: it keeps the cover first both ways, so the cover shuts under pages that are still open and they pass through it.)
  5. Every length is a multiple of one base unit, --u, so the book is the same share of a gallery card, the editor and a recording canvas.
  6. The open book is the size the band has to hold, not the shut one: the pages swing past the spine and lift toward you, where the perspective makes them bigger still. Shut, the book is centred; open, it spreads both ways from the spine. So the translate on the book follows --open: it slides the spine from the middle of the shut book to the middle of the canvas as the book opens, and a paused card, which shows the shut book, shows it centred. The padding above it does the same for the lift.

Key techniques

  • transform-origin: left
  • var() end angle per sheet
  • one @property --open, a staggered slice per sheet

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 55 lines

.scene {
  /* one base unit: every length below is a multiple of it, so the book is the same share of a
     card, the editor, a full screen and a recording canvas */
  --u: 0.28vmin;
  display: grid;
  place-items: center;
  /* the pages lift toward you as they turn, and the perspective magnifies them upward: room
     above the book splits that between the shut book and the open one */
  padding-top: calc(30 * var(--u));
  perspective: calc(1000 * var(--u));
}

.book {
  position: relative;
  width: calc(130 * var(--u));
  height: calc(175 * var(--u));
  border-radius: calc(2 * var(--u)) calc(8 * var(--u)) calc(8 * var(--u)) calc(2 * var(--u));
  background: #4a3a99;               /* back cover */
  transform-style: preserve-3d;
  /* the pages swing left past the spine, so the book slides right as it opens: shut, the book
     is centred, and open, the spine is in the middle and the spread is centred */
  transform: translate(calc(60 * var(--u) * var(--open) + var(--u)), calc(-6 * var(--u))) rotateX(24deg) rotateY(-12deg);
  animation: open 3.6s ease-in-out infinite alternate;
}

.book i {
  --end: calc(-118deg - var(--n) * 12deg);
  position: absolute;
  inset: calc(3 * var(--u)) calc(3 * var(--u)) calc(3 * var(--u)) 0;
  border-radius: 0 calc(5 * var(--u)) calc(5 * var(--u)) 0;
  background: linear-gradient(90deg, #cfd3e6, #fff 14%);
  transform-origin: left center;
  /* a sheet turns while --open runs through its slice: the cover's (n = 4) starts at 0, and each
     sheet under it starts 0.04 later; every slice is 0.84 long */
  --from: calc((4 - var(--n)) * 0.04);
  transform: translateZ(calc(var(--n) * max(1px, var(--u)))) rotateY(calc(var(--end) * clamp(0, (var(--open) - var(--from)) / 0.84, 1)));
}

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

/* how far open the book is, from 0 (shut) to 1; registered, so it animates as a number */
@property --open {
  syntax: '<number>';
  inherits: true;
  initial-value: 0;
}

@keyframes open {
  0%, 10%   { --open: 0; }
  90%, 100% { --open: 1; }
}
Standalone snippet · plain CSS, no build step, no dependencies.