Skip to content
animorio
IT
Back to the library

The train across the page

Scroll, SVGGSAPAdvancedCompressed weight: Demo 20 KBGSAP + ScrollTrigger 45 KBLenis 5.3 KBFreeDownload the zip

Scroll inside the preview

As you scroll, a little steam train rides through a layered landscape: countryside, a bridge over a river, a dark tunnel where the windows light up, the sea at dusk. At every station a ticket card comes in with its text and the name swings on a hanging sign.

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 train across the page</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/index.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/instrument-sans@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main>
    <!-- the ride: taller than the screen; the stage sticks while the landscape rolls past the train -->
    <section class="ride" data-ride>
      <div class="stage" data-stage>
        <svg class="world" data-world aria-hidden="true"></svg>

        <header class="bar">
          <a class="brand" href="#">Larkspur <span>Rail Journeys</span></a>
          <a class="bar__link" href="#">Timetable</a>
        </header>

        <!-- one card per stop; data-sign is the name painted on the swinging station sign -->
        <div class="cards">
          <article class="card card--intro" data-card="intro">
            <p class="card__kicker">Slow trains, long windows</p>
            <h1 class="card__title" data-split>Four stops and one long window seat.</h1>
            <p class="card__hint">Scroll to ride
              <svg class="card__arrow" viewBox="0 0 16 22" aria-hidden="true"><path d="M8 2v17M2 13l6 6 6-6"/></svg>
            </p>
          </article>

          <article class="card" data-card="stop" data-sign="Meadowmere">
            <p class="card__kicker">The countryside</p>
            <h2 class="card__title" data-split>Bread at six, cows at seven.</h2>
            <p class="card__text">A halt so small the train stops on request. Hay bales, a bakery by the level crossing and a guard who waves at every field.</p>
          </article>

          <article class="card" data-card="stop" data-sign="Rivercross">
            <p class="card__kicker">The river</p>
            <h2 class="card__title" data-split>Slow down, the herons are watching.</h2>
            <p class="card__text">The line crosses the Wend on an iron bridge. Pick the left side of the carriage: the water is wide here and the light is soft all morning.</p>
          </article>

          <article class="card" data-card="stop" data-sign="Underhill">
            <p class="card__kicker">The tunnel</p>
            <h2 class="card__title" data-split>Two minutes of dark.</h2>
            <p class="card__text">The carriage lamps come on, somebody always laughs, the tea goes cold. Underhill is the only station on the line built inside a hill.</p>
          </article>

          <article class="card" data-card="stop" data-sign="Saltwick Bay">
            <p class="card__kicker">The coast</p>
            <h2 class="card__title" data-split>Salt on the windows.</h2>
            <p class="card__text">Last stop. Chips on the harbour wall, a lighthouse that winks at the evening train and a sea that is never the same colour twice.</p>
          </article>

          <article class="card card--outro" data-card="outro">
            <p class="card__kicker">Larkspur Rail Journeys</p>
            <h2 class="card__title" data-split>The next train leaves at dawn.</h2>
            <a class="ticket" href="#">
              <span class="ticket__fill" aria-hidden="true"></span>
              <span class="ticket__label">Book a window seat</span>
              <svg class="ticket__arrow" viewBox="0 0 22 14" aria-hidden="true"><path d="M1 7h18M13 1l6 6-6 6"/></svg>
            </a>
          </article>
        </div>
      </div>
    </section>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/ScrollTrigger.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/lenis@1.3.26/dist/lenis.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.

52 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 train across the page” from Animorio and fit it into my site

What it does

The page of an invented rail travel agency, Larkspur Rail Journeys. The stage stays put on the screen and the page is a journey: at the top a cream ticket card says “Four stops and one long window seat.”, and as soon as you scroll a little green and cream steam train rides in from the left with three carriages, silhouetted passengers behind the windows (one in a hat, a child, a dog, someone reading the paper) and the driver in the cab. The landscape rolls behind it in seven layers moving at different speeds: the clouds, the far hills with a bluish town, the hills with trees and a turning windmill, the big trees, fences and hay bales, the track, the telegraph poles with their wires passing in front of the train, and the grass in the foreground, the fastest. The ride slows down and stops at four stations. Meadowmere, in the countryside, with its platform and bench. Rivercross, halfway across an iron truss bridge over a river with reeds: the truss runs both in front of and behind the carriages. Underhill, inside a tunnel: the train goes into the hill, seen cut open, the windows light up yellow one carriage at a time, the carriages go dark and the engine’s lamp opens a beam of light. Saltwick Bay, on the harbour wall, at dusk: coming out of the tunnel the sky is purple and orange, the low sun is mirrored in the sea, the sailing boats rock and the lighthouse sends out its beam. At every station a ticket card comes in with a title and a text, with a little bounce and the words rising one by one, and the station’s name is on a green sign hanging from two chains that swings: braking, the train pushes it forward and it rocks until it settles. The wheels turn with the distance travelled and the engine’s coupling rod goes round, the carriages stretch and bunch up on their couplings when the train speeds up or brakes and rock one after the other, they bump over the rail joints, and the chimney puffs steam every half turn of the wheels (standing still, a lazy puff now and then). At the end the train sets off again, leaves on the right and the last ticket stays, with the “Book a window seat” button.

How it works

The still stage and the script

The section is many screens tall and its first child is a sticky stage, 100lvh tall with overflow: hidden. A paused GSAP timeline carries two numbers: d, the distance travelled (in screen widths), and tx, the train coming in from the left (-1), standing in its place (0) and leaving on the right (1). Between stations d moves on with power2.inOut, then holds for a while: that is the stop. A ScrollTrigger with scrub ties the timeline to the scroll, and Lenis makes it smooth.

The layered landscape

Everything is one SVG as big as the stage, redrawn by JavaScript when its size changes. Each layer is a group moved by -d × its speed (clouds 0.04, far hills 0.14, nearer ones 0.38 and 0.68, the track 1, the poles 1.32, the grass 1.75); SPEED widens or narrows these differences. The things on the track sit where the line meets them: bridge, tunnel, sea and stations are at a fixed distance. The far layers have two versions, countryside and coast, which cross-fade while the train is in the tunnel and the rock covers everything; the same goes for the sky, which turns from day to dusk.

The train

Engine and carriages are SVG groups drawn in code. On every frame (gsap.ticker) the distance the train has travelled against the world is worked out: the wheels turn by distance over radius, the coupling rod follows a circle, speed and acceleration come from there. Each carriage has two springs: one for the coupling, which makes it stretch and bunch up pulled by the one ahead, and one for the rocking, with a different phase per carriage; at the rail joints it bumps by a pixel.

The tunnel and the steam

The hill is drawn in the track layer, so behind the train; the tunnel’s two stone jambs sit in a layer in front, so the train passes under them. For each carriage the code checks whether its centre is inside the tunnel: the windows go towards yellow, a dark veil covers the paint and the glints disappear. The steam is a pool of small clouds: one leaves every half turn of the wheels, stays still in the world (it slides with the track), rises, swells and fades.

The tickets and the signs

Every stop has a time window in the timeline; the ticker looks at where the timeline has got to and shows the right ticket: it comes in with back.out and the title’s words rise one by one, the old one leaves upwards. The station sign is a pendulum hanging from the world: the world’s acceleration pushes it, a spring with friction brings it back down, and when the ticket arrives it gets a little extra push.

Values to change

NameFileDefaultWhat it changes
CARSscript.js3How many carriages the engine pulls, from 1 to 6.
SPEEDscript.js1How far apart the landscape layers move: 0 all together, 1 natural, 1.5 very deep.
SMOKEscript.js1How much steam the engine puffs: 0 none, 2 a lot.
SWAYscript.js1How much the carriages rock and stretch on their couplings: 0 stiff.
SMOOTHscript.js1How many seconds the ride takes to catch up with the scroll.
--train-bodystyle.css#1f5c47The colour of the train’s paint (and of the signs).
--sky-dusk-bottomstyle.css#f4a06cThe colour of the low sky at dusk, over the sea.

Try them in the Playground

When to use it

For the page of a journey, an itinerary, a story in stages: an agency, a festival across several towns, a company’s history year by year, an onboarding path. Every stop is a chapter and the changing landscape keeps the scroll alive.

On phones

Same ride, same animations: the train is bigger compared with the screen and sits further right, so the carriages at the back run off the edge; the ticket takes the top and the sign hangs under it, above the train. The finger scrolls like the wheel.

With reduced motion

The train does not travel: it is already in place and, when you reach a stop, the scene fades to the paper colour and reappears at the new station. The wheels do not turn, the carriages do not rock, no steam, the signs stay still and the tickets fade in. The windows in the tunnel still light up.

Pitfalls

  • With scrub the timeline lags behind the scroll: if the ticket is chosen in the ScrollTrigger’s onUpdate, which fires only when the page moves, the old one stays. It must be chosen on every frame by looking at the timeline’s time.
  • A slow layer, with things placed where the camera will see them, ends up showing the sea in the countryside if its speed is very low. The two versions of the far layers swap when the tunnel covers the whole screen, so the change is never seen.
  • The steam must not be attached to the train: it has to stay still in the world and slide with the track, or at speed it rises straight up like from a house chimney.
  • The hanging sign has to live in the track layer (it stops with the station) but in front of the train: it goes in a group that moves like the track and is drawn after the train.
  • Drawing the stage with pixel measures means redrawing it on every size change; that is why the timeline counts distance in screen widths, not pixels.

Libraries

GSAP 3.15.0, Lenis 1.3.26, Fraunces (font variabile) 5.3.0, Instrument Sans (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 20 KB; the libraries: GSAP + ScrollTrigger 45 KB, Lenis 5.3 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