Skip to content
animorio
IT
Back to the library

The jigsaw transition

Transitions, SVGGSAPAdvancedCompressed weight: Demo 11 KBGSAP 28 KBFreeDownload the zip

A page change on click: the page breaks into real jigsaw pieces, tabs and all, that lift, spin and fly away; the pieces of the new page come in from the sides, turn over and click into place, and the last one drops in with a bounce.

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">
  <title>The jigsaw transition</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bricolage-grotesque@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <!-- two pages of a made-up studio. Click a link and the page breaks into jigsaw pieces that lift, spin and fly
       away; the pieces of the new page come in from the sides, turn over and click into place, and the last one
       drops in with a bounce -->
  <div class="palco" data-palco>

    <!-- the studio page -->
    <section class="pagina pagina--studio" data-pagina="studio" aria-label="Studio">
      <header class="testa">
        <a class="marchio" href="#studio" data-vai="studio" aria-label="Mortise, studio page">
          <svg class="marchio__segno" viewBox="0 0 32 32" aria-hidden="true">
            <path d="M3 3h11v3.2a3 3 0 1 0 4 0V3h11v11h-3.2a3 3 0 1 0 0 4H29v11H18v-3.2a3 3 0 1 0-4 0V29H3V18h3.2a3 3 0 1 0 0-4H3z"/>
          </svg>
          <span>Mortise</span>
        </a>
        <nav class="voci" aria-label="Main">
          <a href="#studio" data-vai="studio" aria-current="page">Studio</a>
          <a href="#work" data-vai="work">Work</a>
        </nav>
        <span class="testa__posta">hello@mortise.studio</span>
      </header>

      <main class="studio">
        <div class="studio__testo">
          <p class="etichetta" data-onda>Independent design studio</p>
          <h1 class="titolo" tabindex="-1" data-onda>We make the pieces fit.</h1>
          <p class="studio__frase" data-onda>Identities, websites and motion for people who build things carefully. Twelve of us, one long table.</p>
          <a class="tessera" href="#work" data-vai="work" data-autoclic data-onda>
            <svg class="tessera__forma" aria-hidden="true"><path data-forma/></svg>
            <span class="tessera__testo">See the work</span>
            <svg class="tessera__freccia" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 12h15M13 6l6 6-6 6"/></svg>
          </a>
        </div>
        <div class="composizione" aria-hidden="true" data-onda>
          <span class="forma forma--sole"></span>
          <span class="forma forma--arco"></span>
          <span class="forma forma--righe"></span>
          <span class="forma forma--quarto"></span>
          <span class="forma forma--blocco"></span>
        </div>
      </main>

      <footer class="piede">
        <span>Brand, web, motion</span>
        <span>Booking projects for spring</span>
      </footer>
    </section>

    <!-- the work page -->
    <section class="pagina pagina--work" data-pagina="work" aria-label="Work" inert>
      <header class="testa">
        <a class="marchio" href="#studio" data-vai="studio" aria-label="Mortise, studio page">
          <svg class="marchio__segno" viewBox="0 0 32 32" aria-hidden="true">
            <path d="M3 3h11v3.2a3 3 0 1 0 4 0V3h11v11h-3.2a3 3 0 1 0 0 4H29v11H18v-3.2a3 3 0 1 0-4 0V29H3V18h3.2a3 3 0 1 0 0-4H3z"/>
          </svg>
          <span>Mortise</span>
        </a>
        <nav class="voci" aria-label="Main">
          <a href="#studio" data-vai="studio">Studio</a>
          <a href="#work" data-vai="work" aria-current="page">Work</a>
        </nav>
        <span class="testa__posta">hello@mortise.studio</span>
      </header>

      <main class="lavori">
        <div class="lavori__testa">
          <div>
            <p class="etichetta" data-onda>Selected work</p>
            <h1 class="titolo" tabindex="-1" data-onda>Four recent fits.</h1>
          </div>
          <a class="tessera" href="#studio" data-vai="studio" data-autoclic data-onda>
            <svg class="tessera__forma" aria-hidden="true"><path data-forma/></svg>
            <span class="tessera__testo">Meet the studio</span>
            <svg class="tessera__freccia" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 12h15M13 6l6 6-6 6"/></svg>
          </a>
        </div>
        <ul class="griglia">
          <li class="lavoro" data-onda>
            <div class="copertina copertina--te" aria-hidden="true"><span></span><span></span></div>
            <p class="lavoro__nome">Halden Tea</p>
            <p class="lavoro__tipo">Identity</p>
          </li>
          <li class="lavoro" data-onda>
            <div class="copertina copertina--bici" aria-hidden="true"><span></span><span></span></div>
            <p class="lavoro__nome">Orbit Bikes</p>
            <p class="lavoro__tipo">Website</p>
          </li>
          <li class="lavoro" data-onda>
            <div class="copertina copertina--libri" aria-hidden="true"><span></span><span></span><span></span></div>
            <p class="lavoro__nome">Paper Moon Press</p>
            <p class="lavoro__tipo">Books</p>
          </li>
          <li class="lavoro" data-onda>
            <div class="copertina copertina--radio" aria-hidden="true"><span></span><span></span></div>
            <p class="lavoro__nome">Lowtide Radio</p>
            <p class="lavoro__tipo">Motion</p>
          </li>
        </ul>
      </main>

      <footer class="piede">
        <span>Brand, web, motion</span>
        <span>Booking projects for spring</span>
      </footer>
    </section>

    <!-- the table under the pages: the pieces fly over it -->
    <div class="tavolo" data-tavolo aria-hidden="true" inert>
      <svg class="tavolo__traccia" data-traccia></svg>
      <div class="tavolo__pezzi" data-pezzi></div>
    </div>
  </div>
  <p class="sr" role="status" aria-live="polite" data-avviso></p>

  <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.

48 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 jigsaw transition” from Animorio and fit it into my site

What it does

Two pages of a made-up design studio, Mortise: the paper-coloured Studio page, with a big headline and a composition of shapes, and the forest-green Work page, with four projects. On opening, the first page puts itself together: the pieces come in from the sides and click into place. On a click on a link (the button shaped like a jigsaw piece, or the menu items) the cut lines of a jigsaw draw themselves on the page, starting from the link you clicked; the pieces lift off the table with a spreading shadow, spin and fly away outwards, some turning over to show their cardboard back. On the dark table the dashed outline of the new puzzle is left. The pieces of the new page come in from the sides upside down and turned, flip over in the air, pause for a moment above their place and drop in with a click, their cut line lighting up. The last piece, the one in the middle, falls from above big and crooked, bounces into its hole and shakes the whole puzzle; the pieces around it give a little hop. Then the lines fade, the pieces become the page again and the words settle with a small wave.

How it works

Every piece is the whole page

On a click the script copies the page (cloneNode) once per piece and cuts each copy with clip-path: path() along the outline of the piece. The copy is as big as the screen and shifted back by the position of the piece, so inside the cut you see exactly that bit of page, words and shapes included. The copies lose data-autoclic, data-vai and tabindex, and the table holding them is inert and aria-hidden: they cannot be clicked or read twice.

The outline with tabs

Every inner edge is made of ten points: a start and three cubic curves (shoulder and neck, head, neck and shoulder). The sizes of the tab are shares of the short side of a piece (KNOB), a little crooked at random (WOBBLE), and flip chooses which way it sticks out. Every edge is worked out once: the piece above walks it backwards (just reverse the list of points), so the two pieces match to the pixel. On a tall screen columns and rows swap.

The cut and the flight

Every piece has an SVG with its outline, two paths (a shade and a light) with pathLength="1": the cut draws itself with stroke-dashoffset from 1 to 0, first near the clicked link and then further away. Then the piece lifts (scale LIFT, a wider drop-shadow, back.out) and flies off along the direction from the link to its centre with power2.in, turning up to SPIN degrees. The pivot is the centre of the cell, not of the box with the tabs.

The back and the click

The new pieces start off screen with a negative scaleX: a piece upside down. A cardboard layer inside the cut turns on while scaleX is below zero (checked by the timeline's onUpdate with gsap.getProperty), so as the piece turns over you see the back first and then the page. Once it is seven pixels above its place, the piece drops with a power3.in keyframe to scale 0.975, the shadow goes out, and it comes back to 1 with elastic.out: that is the click of the fit.

The last piece and the swap

The piece nearest the middle comes last, from above: it starts big (DROP, that is close to us) and crooked, and its scale goes back to 1 with bounce.out. The first contact of bounce.out falls at 1/2.75 of the duration: that is when the table shakes (keyframes on x and y) and the neighbours hop, the nearest first. At the end the cuts fade and in the same frame the real page is shown and the table is emptied: the pieces are already exact to the pixel, so the swap cannot be seen.

Values to change

NameFileDefaultWhat it changes
COLUMNSscript.js4How many pieces across on a wide screen (on a tall screen columns and rows swap).
ROWSscript.js3How many pieces down.
KNOBscript.js0.1The size of the tabs, as a share of the short side of a piece: 0.1 is the classic jigsaw, 0.05 small tabs, 0.13 big ones.
WOBBLEscript.js0.035How irregular the tabs are, so that no two are the same (0 = all perfect).
SPINscript.js70The most a piece turns in flight, in degrees.
LIFTscript.js1.08How much a piece grows when it comes off the table.
DROPscript.js1.7How high the last piece falls from: its size at the start of the fall.
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

For the page change of a playful site: a creative studio, a portfolio, a games and puzzles shop, an exhibition, a campaign about pieces fitting together (teams, partnerships, modular products). It also works between the sections of a single page or the steps of a presentation. Use it where the change is a small event, not on every click of an app.

On phones

The same transition with the sizes adapted: the tall screen is cut into three columns and four rows, so the pieces stay nearly square, and the work page puts the projects two to a row. A tap on the link is enough.

With reduced motion

No pieces and no movement: the old page fades out and the new one fades in. On opening, the first page appears the same way.

Pitfalls

  • Two neighbouring pieces drawn each on its own left gaps and overlaps along the cut: every edge is worked out only once and one of the two pieces walks it backwards.
  • With tabs measured on the length of the edge, on a phone (narrow, tall pieces) two opposite tabs touched in the middle of the piece: they are measured on the short side, centred on the edge.
  • The copies of the page carried data-autoclic along: the autopilot counted the buttons in the copies and clicked those. The copies lose the attributes of the controls.
  • The new page, once inert was removed, showed for one frame before the timeline hid it on its first tick: old page, new page and table are set with gsap.set in the click itself, not inside the timeline.
  • A drop-shadow put on the cut element was cut off by the cut itself: the filter sits on the outer element, the clip-path on the inner one.
  • The neighbours' hop on the landing of the last piece used scale, which on the last pieces to arrive was still springing with elastic.out: the two animations fought over the same property. The hop uses y, which is still by then.

Libraries

GSAP 3.15.0, Bricolage Grotesque (font variabile) 5.3.0.

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

Testing

Tested on 10 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