Pages

No dependencies
Code format
React + Tailwind
const PAGES = [0, 1, 2];

const css = `
  /* Each page lifts from the right, turns over the spine and lies down on the left. */
  @keyframes loader-pages-turn {
    0% { transform: rotateY(0deg); background: light-dark(#50535b, #c6c8cd); }
    10% { transform: rotateY(0deg); }
    45% { background: light-dark(#373a40, #f1f2f3); }
    50% { transform: rotateY(-90deg); background: light-dark(#373a40, #f1f2f3); }
    60% { background: light-dark(#5f626a, #a7abb2); }
    90%, 100% { transform: rotateY(-180deg); background: light-dark(#50535b, #c6c8cd); }
  }
  .loader-pages { position: relative; width: 104px; height: 66px; perspective: 340px; }
  .loader-pages__cover { position: absolute; left: 0; right: 0; bottom: 0; height: 60px; border-radius: 4px; background: light-dark(#dbdde1, #2e3035); box-shadow: 0 10px 18px -10px light-dark(rgba(0, 0, 0, 0.21), rgba(0, 0, 0, 0.7)); }
  .loader-pages__half { position: absolute; top: 2px; bottom: 4px; width: 48px; background: light-dark(#50535b, #c6c8cd); }
  .loader-pages__half--l { left: 4px; border-radius: 3px 0 0 3px; box-shadow: inset -6px 0 8px -6px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); }
  .loader-pages__half--r { right: 4px; border-radius: 0 3px 3px 0; box-shadow: inset 6px 0 8px -6px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); }
  .loader-pages__half::after { content: ''; position: absolute; left: 8px; right: 8px; top: 10px; height: 26px; background: repeating-linear-gradient(light-dark(#6d7179, #7d8087) 0 1.5px, transparent 1.5px 6px); opacity: 0.8; }
  .loader-pages__page {
    position: absolute;
    left: 52px;
    top: 2px;
    bottom: 4px;
    width: 48px;
    border-radius: 0 3px 3px 0;
    background: light-dark(#50535b, #c6c8cd);
    box-shadow: inset 0 0 0 1px color-mix(in oklab, light-dark(#6d7179, #7d8087) 40%, transparent);
    transform-origin: 0 50%;
    animation: loader-pages-turn 2.4s cubic-bezier(0.5, 0, 0.4, 1) infinite;
    animation-delay: calc(var(--k) * 0.8s);
  }
  @media (prefers-reduced-motion: reduce) {
    .loader-pages__page { animation: none; display: none; }
  }
`;

export function PagesLoader() {
  return (
    <>
      <style>
        {css}
      </style>
      <div className="loader-pages" role="status" aria-label="Loading">
        <span className="loader-pages__cover" aria-hidden="true" />
        <span className="loader-pages__half loader-pages__half--l" aria-hidden="true" />
        <span className="loader-pages__half loader-pages__half--r" aria-hidden="true" />
        {PAGES.map((k) => (
          <span key={k} className="loader-pages__page" aria-hidden="true" style={{ '--k': k } as React.CSSProperties} />
        ))}
      </div>
    </>
  );
}

Free to use in any project, commercial included. MIT-0, no attribution required. Licence

Install with shadcn

npx shadcn@latest add @movin/loader-pages
Source on GitHub

About this component

An open book whose pages turn over one after another, each brightening as it lifts past the spine and settling on the left.

Dependencies
None
Difficulty
Beginner
Triggers
continuous, loop, 3d, delay stagger
Techniques
keyframes, 3d transform, perspective, stagger
Reduced motion
Respected
Theme
Light and dark (follows your page's color-scheme)

The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the loose pages are hidden and the book lies open and still.

More loaders

See all
Seam
Sundial
Assemble
Binary