Skip to content
animorio
IT
Back to the library

The road map fold

Transitions, 3DGSAP, CSS onlyAdvancedCompressed weight: Demo 14 KBGSAP 28 KBFreeDownload the zip

The page is a printed road map: a click lifts it off the desk and really folds it into six panels, the two side columns like an accordion and then in half. The little packet jumps, turns over in the air and opens again, panel after panel, on the page printed on its back. The next click folds it up and goes back.

The code

A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN. The React and Vue tabs hold the same demo as a ready component, which switches its animations off when it leaves the page.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
  <title>The road map fold</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bricolage-grotesque@5.3.0/standard.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/instrument-sans@5.3.0/standard.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage" data-stage>

    <!-- the desk under the page: you only see it while the map is folded up -->
    <div class="desk" aria-hidden="true">
      <svg class="desk__lines" data-desk viewBox="0 0 1200 800" preserveAspectRatio="xMidYMid slice"></svg>
    </div>
    <div class="shadow" data-shadow aria-hidden="true"></div>

    <!-- the sheet that folds: three columns and two rows of panels, hinged to each other.
         The middle column holds the other two; every column holds its top and bottom panel.
         The panels are filled with copies of the two pages when a fold starts (page A on the front, page B on the back). -->
    <div class="rig" data-rig aria-hidden="true">
      <div class="col col--m" data-col="1">
        <div class="cell cell--top" data-cell data-c="1" data-r="0"></div>
        <div class="cell cell--bot" data-cell data-c="1" data-r="1"></div>
        <div class="col col--l" data-col="0">
          <div class="cell cell--top" data-cell data-c="0" data-r="0"></div>
          <div class="cell cell--bot" data-cell data-c="0" data-r="1"></div>
        </div>
        <div class="col col--r" data-col="2">
          <div class="cell cell--top" data-cell data-c="2" data-r="0"></div>
          <div class="cell cell--bot" data-cell data-c="2" data-r="1"></div>
        </div>
      </div>
    </div>

    <!-- the two real pages, the ones you read and click while the sheet lies flat -->
    <div class="live" data-live>

      <!-- page A: the home of a road trip guide -->
      <section class="page page--a" data-page="a" aria-label="Byway, slow road trips">
        <header class="bar">
          <span class="brand">
            <svg class="brand__mark" viewBox="0 0 32 32" aria-hidden="true">
              <rect x="1" y="1" width="30" height="30" rx="9" class="brand__tile"/>
              <path d="M11 30 C11 22 21 21 21 15 C21 9 12 9 13 2" class="brand__road"/>
              <path d="M11 30 C11 22 21 21 21 15 C21 9 12 9 13 2" class="brand__dash"/>
            </svg>
            <span class="brand__name">Byway</span>
          </span>
          <nav class="links" aria-label="Byway">
            <a href="#routes" class="link"><span class="link__plate" aria-hidden="true"></span><span class="link__text">Routes</span></a>
            <a href="#stays" class="link"><span class="link__plate" aria-hidden="true"></span><span class="link__text">Stays</span></a>
            <a href="#journal" class="link"><span class="link__plate" aria-hidden="true"></span><span class="link__text">Journal</span></a>
          </nav>
          <span class="bar__tag">Spring routes</span>
        </header>

        <div class="hero">
          <div class="hero__text">
            <p class="kicker">Slow road trips on the Northern Coast</p>
            <h1 class="title">Take the <em>long way</em> round.</h1>
            <p class="lede">Hand-drawn routes along the coast road, with the bakeries, lay-bys and empty beaches worth stopping for.</p>
            <button class="cta" type="button" data-go data-autoclic>
              <span class="cta__bg" aria-hidden="true"></span>
              <span class="cta__label">Open the Coast Road</span>
              <span class="cta__map" aria-hidden="true"><i></i><i></i><i></i></span>
            </button>
            <dl class="facts">
              <div class="fact"><dt>Coast road</dt><dd>412 km</dd></div>
              <div class="fact"><dt>Stops</dt><dd>7</dd></div>
              <div class="fact"><dt>Days</dt><dd>6</dd></div>
            </dl>
          </div>

          <figure class="mapcard" data-card>
            <svg class="map" viewBox="0 0 600 500" preserveAspectRatio="xMidYMid meet" role="img" aria-label="Map of the Coast Road from Wren Harbour to Saltmere">
              <rect x="-600" y="-400" width="1800" height="1300" class="map__sea"/>
              <g class="map__waves">
                <path d="M40 70 q10 -6 20 0 t20 0"/><path d="M96 128 q10 -6 20 0 t20 0"/><path d="M30 260 q10 -6 20 0 t20 0"/>
                <path d="M60 352 q10 -6 20 0 t20 0"/><path d="M150 40 q10 -6 20 0 t20 0"/><path d="M22 440 q10 -6 20 0 t20 0"/>
              </g>
              <path d="M54 190 C40 172 66 156 86 166 C108 176 104 202 84 206 C70 210 62 200 54 190Z" class="map__land"/>
              <path d="M262 0 C244 46 196 70 204 122 C212 170 158 196 166 248 C174 300 116 326 128 380 C138 424 100 452 108 500 C112 560 90 640 96 900 H1200 V-400 H300 C286 -260 276 -90 262 0Z" class="map__land"/>
              <path d="M300 -400 C286 -260 276 -90 262 0 C244 46 196 70 204 122 C212 170 158 196 166 248 C174 300 116 326 128 380 C138 424 100 452 108 500 C112 560 90 640 96 900" class="map__beach"/>
              <g class="map__hills">
                <ellipse cx="476" cy="150" rx="92" ry="52"/><ellipse cx="482" cy="146" rx="62" ry="34"/><ellipse cx="488" cy="142" rx="32" ry="17"/>
                <ellipse cx="452" cy="378" rx="78" ry="40"/><ellipse cx="458" cy="374" rx="46" ry="22"/>
              </g>
              <path d="M514 236 C530 222 560 226 566 244 C584 248 586 274 568 282 C560 300 528 298 518 284 C498 284 494 252 514 236Z" class="map__wood"/>
              <path d="M346 456 C356 442 384 444 390 458 C402 462 400 482 386 484 C376 496 350 494 346 480 C332 478 334 460 346 456Z" class="map__wood"/>
              <ellipse cx="384" cy="262" rx="30" ry="15" class="map__lake"/>
              <path d="M332 22 C304 52 254 74 262 124 C270 174 214 198 224 250 C234 302 174 328 188 380 C198 420 168 446 186 484" class="map__road" data-road/>
              <path d="M332 22 C304 52 254 74 262 124 C270 174 214 198 224 250 C234 302 174 328 188 380 C198 420 168 446 186 484" class="map__dash" data-road-dash/>
              <g class="map__scale"><path d="M470 470 H550"/><path d="M470 465 V475 M550 465 V475 M510 467 V473"/><text x="510" y="460">20 km</text></g>
              <g class="pin" data-pin data-at="0.04"><g class="pin__body"><path d="M0 0 C-7 -10 -11 -15 -11 -21 A11 11 0 1 1 11 -21 C11 -15 7 -10 0 0Z"/><circle cy="-21" r="4.2"/></g><text x="16" y="-14">Wren Harbour</text></g>
              <g class="pin" data-pin data-at="0.35"><g class="pin__body"><path d="M0 0 C-7 -10 -11 -15 -11 -21 A11 11 0 1 1 11 -21 C11 -15 7 -10 0 0Z"/><circle cy="-21" r="4.2"/></g><text x="16" y="-14">Lantern Cliffs</text></g>
              <g class="pin" data-pin data-at="0.66"><g class="pin__body"><path d="M0 0 C-7 -10 -11 -15 -11 -21 A11 11 0 1 1 11 -21 C11 -15 7 -10 0 0Z"/><circle cy="-21" r="4.2"/></g><text x="16" y="-14">Gull Bay</text></g>
              <g class="pin" data-pin data-at="0.88"><g class="pin__body"><path d="M0 0 C-7 -10 -11 -15 -11 -21 A11 11 0 1 1 11 -21 C11 -15 7 -10 0 0Z"/><circle cy="-21" r="4.2"/></g><text x="16" y="-14">Saltmere</text></g>
              <g class="car" data-car>
                <g data-car-in>
                  <rect x="-12" y="-6.5" width="24" height="13" rx="4.5" class="car__body"/>
                  <rect x="2" y="-5" width="5.5" height="10" rx="1.6" class="car__glass"/>
                  <rect x="-9" y="-5" width="4" height="10" rx="1.4" class="car__glass"/>
                </g>
              </g>
            </svg>
          </figure>
        </div>
        <div class="creases" aria-hidden="true"><i class="crease crease--v1"></i><i class="crease crease--v2"></i><i class="crease crease--h"></i></div>
      </section>

      <!-- page B: the guide to one route, printed on the back of the same sheet -->
      <section class="page page--b is-off" data-page="b" aria-label="The Coast Road">
        <header class="bar">
          <span class="brand">
            <svg class="brand__mark" viewBox="0 0 32 32" aria-hidden="true">
              <rect x="1" y="1" width="30" height="30" rx="9" class="brand__tile"/>
              <path d="M11 30 C11 22 21 21 21 15 C21 9 12 9 13 2" class="brand__road"/>
              <path d="M11 30 C11 22 21 21 21 15 C21 9 12 9 13 2" class="brand__dash"/>
            </svg>
            <span class="brand__name">Byway</span>
          </span>
          <button class="back" type="button" data-back data-autoclic>
            <span class="back__bg" aria-hidden="true"></span>
            <svg class="back__arrow" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 12 H5 M11 5 L4 12 L11 19"/></svg>
            <span class="back__label">All routes</span>
          </button>
        </header>

        <div class="route">
          <div class="route__head">
            <p class="kicker">Route guide, spring edition</p>
            <h2 class="title">The Coast Road</h2>
            <p class="lede">Six days from Wren Harbour to Saltmere, never more than a mile from the sea.</p>
            <figure class="profile">
              <svg class="profile__svg" viewBox="0 0 600 170" preserveAspectRatio="none" aria-hidden="true">
                <path class="profile__area" data-area/>
                <path class="profile__line" data-line/>
              </svg>
              <figcaption class="profile__cap">
                <span>Wren Harbour</span>
                <span class="profile__peak" data-peak>Highest point: Lantern Cliffs, 212 m</span>
                <span>Saltmere</span>
              </figcaption>
            </figure>
          </div>

          <ol class="legs">
            <li class="leg"><span class="leg__day">Monday</span><span class="leg__way">Wren Harbour<svg class="leg__arrow" viewBox="0 0 40 12" aria-hidden="true"><path d="M1 6 H37 M31 1.5 L37 6 L31 10.5"/></svg>Lantern Cliffs</span><span class="leg__km">64 km</span></li>
            <li class="leg"><span class="leg__day">Tuesday</span><span class="leg__way">Lantern Cliffs<svg class="leg__arrow" viewBox="0 0 40 12" aria-hidden="true"><path d="M1 6 H37 M31 1.5 L37 6 L31 10.5"/></svg>Gull Bay</span><span class="leg__km">58 km</span></li>
            <li class="leg"><span class="leg__day">Wednesday</span><span class="leg__way">Gull Bay<svg class="leg__arrow" viewBox="0 0 40 12" aria-hidden="true"><path d="M1 6 H37 M31 1.5 L37 6 L31 10.5"/></svg>Fennick</span><span class="leg__km">81 km</span></li>
            <li class="leg"><span class="leg__day">Thursday</span><span class="leg__way">Fennick<svg class="leg__arrow" viewBox="0 0 40 12" aria-hidden="true"><path d="M1 6 H37 M31 1.5 L37 6 L31 10.5"/></svg>Old Ferry</span><span class="leg__km">72 km</span></li>
            <li class="leg"><span class="leg__day">Friday</span><span class="leg__way">Old Ferry<svg class="leg__arrow" viewBox="0 0 40 12" aria-hidden="true"><path d="M1 6 H37 M31 1.5 L37 6 L31 10.5"/></svg>Marram Dunes</span><span class="leg__km">69 km</span></li>
            <li class="leg"><span class="leg__day">Saturday</span><span class="leg__way">Marram Dunes<svg class="leg__arrow" viewBox="0 0 40 12" aria-hidden="true"><path d="M1 6 H37 M31 1.5 L37 6 L31 10.5"/></svg>Saltmere</span><span class="leg__km">68 km</span></li>
          </ol>
        </div>
        <div class="creases" aria-hidden="true"><i class="crease crease--v1"></i><i class="crease crease--v2"></i><i class="crease crease--h"></i></div>
      </section>
    </div>

    <p class="sr-only" aria-live="polite" data-say></p>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
  <script src="script.js"></script>
</body>
</html>

Playground

Change values, colours and text with the controls and watch the animation respond live. Or write in the code yourself. Then download the folder with your changes.

78 values to change

Or ask Claude Code for it

With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:

Take “The road map fold” from Animorio and fit it into my site

What it does

On load a folded map drops on the dark desk, with its drawn contour lines, turning over in the air; it lands with a little squash and opens: first the bottom half, then the two columns, each with a bounce, and at last it lies down and becomes the whole page. It is the home of Byway, an invented guide to slow road trips, with the title «Take the long way round.» and a map of the coast: a little car drives the road and draws it behind itself, and at every stop a pin drops in with a bounce. The folds stay on the paper as faint lines, a valley and a ridge. A click on «Open the Coast Road» (or on the map) makes the page give a little and the creases deepen, then it comes away from the desk, gets smaller and leans back, its shadow spreading and blurring. The left column closes over the middle one and the right one under it, like an accordion, and while they turn you see a piece of the other page on their back; then the strip folds in half. Every panel takes its own light from the way it is turned, and the folded panels lie one on top of the other with the thickness of the paper. The packet jumps, turns over in the air with a crooked half turn and drops back with a little squash, then opens in the reverse order with the springiness of paper and shows the page printed on the back: the route guide «The Coast Road», in night blue, with the six legs day by day and the height profile growing with an elastic while its line draws itself. «All routes» folds it up and turns it back onto the home. The packet always shows pieces of the page that is leaving, on both sides (even upside down, as on a real map): the new one only appears as it opens.

How it works

Six hinged panels

The sheet is a container with transform-style: preserve-3d. The middle column is the base and holds the other two: the left one has its transform-origin on its right edge, the right one on its left edge, so turning with rotateY closes them right along the crease. In every column the bottom panel has its transform-origin on its top edge and folds with rotateX. Every panel has two faces with backface-visibility: hidden, the front and the back turned by 180 degrees.

The two pages printed on the panels

The real pages sit above the sheet and work as usual. When the fold starts they are copied (cloneNode): every face holds the whole page, as wide and tall as the screen, moved so that only its own piece shows. The front shows page A, the back page B with the columns swapped (column 0 shows piece 2), because after the turn the sheet is seen from behind. The copies lose data-autoclic and the touches that come later (road, pins, profile).

The thickness of the paper

The folds go to exactly 180 degrees, but every hinge turns around a line set off from the paper by half a thickness: translateZ, the rotation, translateZ back. So every layer lands at its own depth and the panels never touch (no flicker). The order of the layers is that of a real map, and the bottom half folds forwards or backwards depending on the page that is leaving, so both sides of the packet always show that one.

One object for the whole sheet

Scale, lean, turn, twist, jump and the three fold angles live in one object (st) that GSAP animates with a timeline: it lifts (power3), closes (power2.inOut), jumps and turns, opens with back.out and lies down. On every update a function writes the transforms and works out the light: the normal of every panel, turned like the panel, against a lamp up on the left, gives the shade or the sheen over its two faces.

The shadow on the desk

The shadow is a separate element on the desk, outside the 3D. Its size is the footprint of the sheet, worked out from the cosines of the angles (a closed column no longer counts) and narrowed by the cosine of the turn, so edge-on it is a line. The higher the sheet lifts or jumps, the more the shadow moves away, blurs and fades. When the new page arrives the sheet goes and the real page, identical, comes back and starts its own touches.

Values to change

NameFileDefaultWhat it changes
FOLDscript.js0.6The seconds of each fold of the map.
SIZEscript.js0.66How big the folded map stays in the air (1 = the whole screen); a little bigger on tall screens.
TILTscript.js18How many degrees the folded map leans back.
HOPscript.js1How high the packet jumps when it turns over (0 = it turns in place).
BOUNCEscript.js1How far the panels overshoot and spring back as they open.
CREASEscript.js0.55How much the creases show on the open page (0 = none).
SPEEDscript.js1The speed of the whole transition: 2 is twice as fast, 0.5 half as fast.

Try them in the Playground

When to use it

To go from the home to a detail page on a travel site, for road or cycling routes, a tourist office, a publisher of guides and maps; for an archive of maps, an outdoor event with its route, whenever the new page is «the back» of the one before.

On phones

The same fold with the sizes adapted: both pages stack in a column and the folded sheet, a thin strip on a tall screen, stays a little bigger in the air. A tap on the button or the map starts it; the hovers (sign plate, opening map, legs) are for the mouse only.

With reduced motion

Nothing folds or moves: the page fades in already complete, with the road, the pins and the car in place; on a click the page fades out and the new one fades in, complete as well.

Pitfalls

  • With folds at 180 degrees the panels end up on the same plane and flicker; at 177 degrees they fan out, but the fans cross near the hinges and one panel pokes through another. The fix is a hinge set off by half a thickness (translateZ before and after the rotation): parallel layers, a few pixels apart.
  • If every bottom half folds around its own column as it comes, the order of the layers is not that of real paper and both outer faces of the packet show the new page. The depths are written layer by layer, and the bottom half goes forwards when page A leaves and backwards when page B leaves.
  • The copies of the pages carry data-autoclic with them: the preview autopilot would have found fourteen buttons instead of two. The attribute is removed in the copies, and the sheet is inert, so nothing in it can be reached with Tab.
  • Selectors find the copies too: «.page--a .cta» finds seven buttons, and the first is a hidden copy in the sheet. The script looks everything up once, before making copies, and only inside the real pages.
  • An id inside the pages (a gradient, a mask) would be repeated twelve times in the copies: the pages use none, the paper grain is a data: SVG in the background.
  • GSAP writes the transforms of an SVG element into its transform attribute: the car, which the script places on the road with setAttribute, has an inner group for GSAP's scale, and the pins one for their drop, separate from the one that swells on hover with CSS transitions.
  • A line drawn with a dasharray equal to its length leaves a dot at the end (the round cap of the next dash): the gap has to be twice as long.

Libraries

GSAP 3.15.0, Bricolage Grotesque (font variabile) 5.3.0, Instrument Sans (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 14 KB; the libraries: GSAP 28 KB.

Testing

Tested on 13 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: To do

License

Use it in your own projects and in your clients’ projects. Do not resell it or redistribute it as is. GSAP stays under its own Standard license. Full license.

All animations