Pure CSS Scenes & objects #loop

Folding map in pure CSS

Nested panels, each hinged on its parent’s edge, folding up like a paper map.

  • 59 lines of CSS
  • 11 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. Panels are nested, not siblings: each one lives inside the previous one, so it inherits every fold before it.
  2. Each nested panel is positioned at left: 100% with transform-origin: left — a hinge on the parent’s right edge.
  3. Every level needs transform-style: preserve-3d, or the chain flattens at that level.
  4. Alternate the fold direction (+150° / −150°) for a zig-zag.
  5. The first panel never moves, so the folded map is a stack at the left end. The map slides right as it folds, on the same timing, so the flat map and the folded stack are both centred, and lifts a little halfway, while the panels stand out toward you.
  6. Every length is a multiple of one base unit, --u, tied to the canvas: the map is four panels of 60 × 170 units, so it is the same share of a gallery card, the editor and a recording canvas.

Key techniques

  • nested preserve-3d
  • transform-origin: left
  • alternating keyframes

Copy-paste code

HTML 11 lines

<div class="scene">
  <div class="map">
    <div class="panel">
      <div class="panel">
        <div class="panel">
          <div class="panel"></div>
        </div>
      </div>
    </div>
  </div>
</div>

CSS 59 lines

.scene {
  /* one base unit: every length below is a multiple of it, so the map is the same share of a
     gallery card, the editor, a full screen and a recording canvas */
  --u: 0.32vmin;
  display: grid;
  place-items: center;
  perspective: calc(900 * var(--u));
}

.map {
  width: calc(240 * var(--u));          /* 4 panels × 60 units */
  height: calc(170 * var(--u));
  transform-style: preserve-3d;
  transform: rotateX(28deg) rotateY(-8deg);
  /* the first panel stays put and the others fold onto it, so the folded map is a stack at the
     left end: the map slides right as it folds, on the same timing, to keep the stack centred */
  animation: slide 3.4s ease-in-out infinite alternate, dip 3.4s ease-in-out infinite alternate;
}

.panel {
  box-sizing: content-box; /* a panel's numbers are its face, its edge drawn round it: by design, on any page */
  width: calc(60 * var(--u));
  height: 100%;
  transform-style: preserve-3d;
  background: linear-gradient(90deg, #2ee6d6, #157a72);
  border: calc(1 * var(--u)) solid rgb(255 255 255 / 0.25);
}

.panel .panel {
  position: absolute;
  top: calc(-1 * var(--u));
  left: 100%;
  transform-origin: left center;
  animation: fold-back 3.4s ease-in-out infinite alternate;
}

.panel .panel .panel        { animation-name: fold-front; }
.panel .panel .panel .panel { animation-name: fold-back; }

@keyframes slide {
  0%, 12%   { translate: calc(-5 * var(--u)) calc(-2 * var(--u)); }
  88%, 100% { translate: calc(78 * var(--u)) calc(-24 * var(--u)); }
}

/* half folded, the panels stand out toward you and reach down: lift the map while they do */
@keyframes dip {
  0%, 12%, 88%, 100% { transform: rotateX(28deg) rotateY(-8deg); }
  50%                { transform: translateY(calc(-12 * var(--u))) rotateX(28deg) rotateY(-8deg); }
}

@keyframes fold-back {
  0%, 12%   { transform: rotateY(0deg); }
  88%, 100% { transform: rotateY(150deg); }
}

@keyframes fold-front {
  0%, 12%   { transform: rotateY(0deg); }
  88%, 100% { transform: rotateY(-150deg); }
}
Standalone snippet · plain CSS, no build step, no dependencies.