Skip to content
animorio
IT
Back to the library

The words-that-stay transition

Transitions, TextGSAP, CSS onlyMediumCompressed weight: Demo 8.8 KBGSAP 28 KBFreeDownload the zip

A minimal page change: the words the two pages have in common never leave. On a click the others fade in a wave, and the ones that stay fly one after the other to their place on the new page, growing or shrinking and changing weight; they land on a thin line, take a touch of weight from the knock and give it back, then the new words rise and the red full stop drops in last.

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 words-that-stay transition</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/roboto-flex@5.3.0/standard.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage" data-stage>
    <!-- the header is the same on both pages, so it never moves -->
    <header class="bar">
      <span class="brand">Quarra</span>
      <nav class="nav" aria-label="Pages">
        <a href="#index" data-go="a" data-nav="a" aria-current="page">Index</a>
        <a href="#rooms" data-go="b" data-nav="b">Rooms</a>
      </nav>
    </header>

    <!-- the first page. The words marked with the same data-share on both pages are the ones that stay:
         they fly to their place on the other page, changing size and weight -->
    <section class="page page--a is-on" data-page="a" aria-label="Index">
      <div class="col col--a">
        <h1 class="headline" data-words><span data-share="quiet">Quiet</span> <span data-share="rooms">rooms</span><br>for loud cities<span class="dot" data-share="dot">.</span></h1>
        <p class="lede" data-words>An architecture studio in <span data-share="porto">Porto</span>: <span data-share="twelve">twelve</span> <span data-share="houses">houses</span>, one school, no towers.</p>
        <a class="go go--next" href="#rooms" data-go="b" data-autoclic data-unit>
          <span class="go__text">See the rooms</span>
          <span class="go__arrow" aria-hidden="true"><span class="go__shaft"></span><svg class="go__head" viewBox="0 0 8 12"><path d="M1.5 1.5 6.5 6l-5 4.5"/></svg></span>
        </a>
      </div>
    </section>

    <!-- the second page -->
    <section class="page page--b" data-page="b" aria-label="Rooms" aria-hidden="true">
      <div class="col col--b">
        <p class="sub" data-words><span data-share="quiet">Quiet</span> <span data-share="houses">houses</span>, <span data-share="twelve">twelve</span> of them.</p>
        <p class="lede" data-words>Each one built in <span data-share="porto">Porto</span> around a single window, and a room left empty on purpose.</p>
        <a class="go go--back" href="#index" data-go="a" data-autoclic data-unit>
          <span class="go__arrow" aria-hidden="true"><svg class="go__head" viewBox="0 0 8 12"><path d="M6.5 1.5 1.5 6l5 4.5"/></svg><span class="go__shaft"></span></span>
          <span class="go__text">Back to the index</span>
        </a>
      </div>
      <h2 class="title" data-words><span data-share="rooms">rooms</span><span class="dot" data-share="dot">.</span></h2>
    </section>

    <footer class="foot">
      <span>Since 2009</span>
      <span>Visits by appointment</span>
    </footer>

    <!-- the words in flight and the baselines they land on (built and moved by the script) -->
    <div class="fly" data-fly aria-hidden="true"></div>
  </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.

39 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 words-that-stay transition” from Animorio and fit it into my site

What it does

The site of a made-up architecture studio, Quarra, in two pages and almost nothing else: a paper-coloured background, near-black ink and a single red dot. On the first page the headline, hairline thin, says Quiet rooms for loud cities., with a small line about the studio under it and the link See the rooms. On load two thin lines draw themselves where the letters will stand, the words rise one after the other, and the full stop drops from above, squashes on the line and springs back round. On a click the words the new page does not use fade in a wave that starts from the link; the ones it needs too (Quiet, rooms, twelve, houses, Porto and the full stop) lift a hair and set off one after the other, each on its own curve. As they fly they change size and weight: Quiet goes from the thin headline to a firm medium line, twelve and houses from the small grey text to the big black line, rooms becomes the huge, very black title at the foot of the page. In the air they narrow a little, like someone running, and widen again as they arrive. Under them the baselines of the new page draw themselves; each word lands with a small squash, takes a touch of extra weight from the knock and springs it back, and the line feels the blow. Then the new words rise into place, the lines draw back, and last of all the red full stop, as big as a letter of the title, drops in next to rooms. Back to the index makes the same journey the other way.

How it works

The words in common

In the HTML the words that stay are marked with the same data-share on both pages (data-share="rooms" in the first page's headline and in the second page's title). The script splits everything with data-words into words: each word becomes an inline-block span, so it can move without pushing its neighbours, while the spaces stay text and the lines break as before. Words without a twin are the ones that leave or arrive.

Everything needed to draw them again

On a click, for every pair the script reads the word it starts from and the word it lands on: the x, the baseline (with an empty, zero-height element placed inside the word), the size, the weight, the width (font-stretch), the letter spacing in em and the colour. Both pages are always laid out, even the one hidden with visibility: hidden, so both are measured before anything moves.

The flight

In place of the real word a copy flies in a layer above the pages, with line-height 1: that way its baseline always sits at a fixed share of its size (measured once). One tween takes t from 0 to 1 and on every frame the copy works everything out again: x with power4.inOut, y with expo.inOut (two different curves bend the path) plus an upward arc, size, weight, spacing and colour with power3.inOut, and the width dropping by SQUEEZE halfway with a sine. Roboto Flex has an optical size axis, so a word that grows changes its drawing too.

The landing

Just before it arrives the copy squashes on the baseline (a scale whose origin sits right on the baseline), drops one pixel and takes on LANDING extra weight in 0.08 seconds; then elastic.out(1, 0.42) gives back shape and weight, with a one-pixel overshoot. The line under it makes the same one-pixel jump and springs back. Once it has settled the real word appears and the copy goes: they have the same size, weight and place, so the swap cannot be seen.

Direction, phone and reduced motion

Each change is one timeline: the wave of leaving words (a stagger worked out from the distance to the click), the flights spaced by STAGGER in the reading order of the new page, the baselines, the new words rising once the travellers are nearly there, and the full stop last; SPEED is the timeScale. A click that comes while the page is changing waits in a queue. The two big lines fit the width and height of the screen and shrink if they would touch the rest. With reduced motion the staying words do not fly: the old page fades out and the new one fades in, already set.

Values to change

NameFileDefaultWhat it changes
FLYscript.js1.1The seconds each word takes to fly to its new place.
STAGGERscript.js0.08The seconds between one word setting off and the next: 0 = they all go together.
ARCscript.js0.16How much the flight bends upward, compared with the distance: 0 = a straight line.
SQUEEZEscript.js24How much a word narrows while it flies (on the width axis): 0 = it keeps its width.
LANDINGscript.js90The extra weight a word takes on as it lands, then gives back: 0 = no knock.
GUIDESscript.jstrueThe baselines of the new page drawing themselves under the arriving words.
SPEEDscript.js1The speed of everything: 2 is twice as fast, 0.5 half as fast.

Try them in the Playground

When to use it

To move from one page to another on a quiet site where words matter: an architecture or design studio, a portfolio, a magazine, the home page leading to a section or a project, a list leading to its detail page. It works best when the two pages really share a few words: the name of a section, a project's title, a key word going from small to big.

On phones

The same transition with the sizes adapted: the first page's headline and the second page's title fit the width of the screen, the text wraps, and the words still fly between the two layouts with the same curves and landings. A tap on the link starts it.

With reduced motion

Nothing moves: the first page fades out, the staying words appear already in place on the second, then the rest, all with fades; going back the same the other way. The entrance is a fade.

Pitfalls

  • A word that changes size inside a line of text would push all its neighbours as it grows. So a copy flies in a separate layer, placed with transform, and the real word stays transparent in its place.
  • A word's box (getBoundingClientRect) depends on the line height and the font, so it cannot line up two different sizes: the baseline is used instead, measured with an empty, zero-height element sitting on the baseline inside the word.
  • The entrance baselines were measured after the words had been pushed 18 pixels down to rise: the lines ended up under the letters. They are measured before anything moves.
  • The big full stop arrived while rooms was still swollen with its landing weight, and for a moment it sat on top of the s: it now leaves later, once rooms has given the weight back. In the title it also has a small left margin, because the tight spacing glued it to the letter.
  • The baselines under the small text were too close to each other and looked like underlines: there are only lines under the big ones.
  • The previews' autopilot clicks every 2.8 seconds and a change lasts about three: a click that comes halfway waits in a queue and goes as soon as the timeline ends.

Libraries

GSAP 3.15.0, Roboto Flex (font variabile) 5.3.0.

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

Testing

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