Skip to content
animorio
IT
All components

The tram nav

Navigation, Physics, Hover, TextCanvas, GSAPAdvancedCompressed weight: Demo 10 KBGSAP 28 KBFreeDownload the zip

The top navigation bar of a heritage tram company. The bar is the ceiling of the line: the contact wire hangs under it and under the wire a double-ended tram, cream and tomato red, runs on a rail and stops under the page you are on, its pantograph pressed against the wire. Choosing another link the bell rings, the tram pulls away leaning back, sparks fly from the pantograph, it brakes with sparks under the wheels, sways on its springs and opens its doors. Pointing at a link makes the wire over it shake.

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 tram nav</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bricolage-grotesque@5.3.0/wdth.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the top bar of a heritage tram company. The bar is the ceiling of the line: the contact wire hangs under it,
         and under the wire a double-ended tram runs on its rail to the page you are on, its pantograph sliding along
         the wire. Choosing another link the bell rings, the tram pulls away leaning back, sparks fly where the
         pantograph touches the wire, it brakes with sparks under the wheels, sways on its springs and opens its doors.
         Pointing at a link plucks the wire over it. -->
    <div class="line" data-line>
      <header class="bar" data-bar>
        <a class="brand" href="#" aria-label="Brassline heritage trams, home">
          <svg class="brand__mark" viewBox="0 0 26 22" aria-hidden="true">
            <path class="brand__wire" d="M1 2.2h24"/>
            <path class="brand__pole" d="M13 2.5l-3.4 4.6h6.8z"/>
            <rect class="brand__body" x="3" y="7.4" width="20" height="9.6" rx="2.4"/>
            <path class="brand__glass" d="M6 9.6h4v3.2H6zM11.2 9.6h3.6v3.2h-3.6zM16 9.6h4v3.2h-4z"/>
            <path class="brand__rail" d="M1 20.4h24"/>
            <circle class="brand__wheel" cx="8" cy="18.2" r="1.6"/>
            <circle class="brand__wheel" cx="18" cy="18.2" r="1.6"/>
          </svg>
          <span class="brand__name">Brassline</span>
        </a>

        <nav class="nav" aria-label="Main">
          <ul class="list" data-list>
            <li class="item"><a class="link" href="#" aria-current="page" data-link>Routes</a></li>
            <li class="item"><a class="link" href="#" data-link data-autoclic>Tours</a></li>
            <li class="item"><a class="link" href="#" data-link data-autoclic>Tickets</a></li>
            <li class="item"><a class="link" href="#" data-link data-autoclic>Depot</a></li>
          </ul>
        </nav>

        <a class="cta" href="#" data-cta><span class="cta__label">Book a ride</span></a>
      </header>
      <canvas class="track" aria-hidden="true" data-track></canvas>
    </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.

27 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 tram nav” from Animorio and fit it into my site

What it does

On a white background a bottle green bar 64 pixels tall: on the left a small drawn tram and the name Brassline, in the middle four links, Routes, Tours, Tickets and Depot, on the right the brass button Book a ride. Under the bar hangs the contact wire on short hangers, and lower down runs a steel rail on wooden sleepers. Under the current link stands a double-ended tram: a cream upper half with six dark glass windows, a tomato red lower half with a brass line, a grey roof with the resistor boxes, two trucks with leaf springs and eight spoked wheels, two-leaf doors in the middle with brass handrails, the company's oval plates, the destination blinds, a cream lamp at the front and a red one at the back, and on the roof the arm pantograph pushing the wire up a little. Inside are the shapes of the passengers and the driver in the front cab. On arrival the tram comes in from the left. Choosing another link the word gains weight and width, the bell rings (two arcs over the cab), the front lamp turns to the new direction, the tram gathers speed leaning back and the passengers lean a moment later; the wheels turn by the distance run, a glint slides across the glass, the lamp's beam lies on the rail ahead, and sparks fly back from the pantograph running along the wire. Braking, the body pitches forward and sparks fly from under the wheels and bounce on the rail; arriving it goes a little too far, comes back, sways on its springs, then the doors open showing the warm light inside and shut again. With the mouse on a link the wire over it shakes. When nothing moves nothing is drawn.

How it works

The tram on a spring with a brake

The tram's position is a spring towards the stop, damped at 0.86 with its frequency taken from RIDE_TIME, but its push is capped at 2600/RIDE_TIME² pixels a second squared: from far away the tram does not jump, it gathers speed, runs and brakes, and arrives going just a little too far. The step is 1/120 of a second, never more than 1/30 a frame.

The body on its springs, the passengers after

The acceleration becomes the angle the body wants (back when it pulls away, forward when it brakes), chased by a spring with a little damping; the passengers follow the body with another, softer spring, so they lean a moment later. On arrival a push upwards and one sideways make the body sway on its leaf springs.

The pantograph on the wire

The pantograph is drawn in world coordinates: its foot follows the tilting roof, its head stays on the wire. The wire is a curve worked out point by point: a small bell-shaped bow where the pantograph presses it, plus the shakes left by the links pointed at, each a sine fading inside a bell. While the tram runs fast the head has a little roughness on a stiff spring.

Real sparks, not dots

Above 70 pixels a second, with a chance that grows with speed and SPARKS, sparks fly back from the contact with the wire; when the tram brakes hard they fly up from the wheels. Every spark has a speed, gravity and a life of a few tenths of a second, bounces on the rail losing strength and is drawn as a short line in the direction it flies, white and then orange.

Details that change with the run

The wheels turn by the distance run over their radius, the glint on the glass slides with the position, the cream front lamp and the red tail lamp swap with the direction, the beam on the rail grows with speed, the bell is two arcs that open and fade. The doors are two leaves that slide with GSAP after the stop and show the warm light inside.

The drawing sleeps

The requestAnimationFrame loop stops when the tram is still, the springs are at rest, there are no sparks or shakes and GSAP is not moving the doors; every event wakes it. The stops are the middles of the links measured with getBoundingClientRect, measured again on resize.

Values to change

NameFileDefaultWhat it changes
RIDE_TIMEscript.js1.2About how many seconds the tram takes to ride under a new link.
SPARKSscript.js1How many sparks fly from the wire and the brakes (0, none).
SWAYscript.js1How much the body leans and sways on its springs (0, it stays level).
WIRE_PLUCKscript.js1How much the wire over a link pointed at shakes (0, still).
DOOR_PAUSEscript.js0.9How many seconds the doors stay open at the stop (0, they stay shut).
TRAM_SIZEscript.js1The size of the tram.

Try them in the Playground

When to use it

For the header of anyone dealing with cities and transport: a heritage tram or sightseeing company, a transport museum, a public transport app, a city centre hotel, a city tour, a site for events around town. It suits a few short links; the colours of the bar and the tram change in the variables.

On phones

Below 640 pixels the bar is 60 pixels tall, the name and the button go, the small drawn tram stays and the four links spread across the whole width. The tram, the wire and the rail are the same, and a finger down on a link shakes the wire like the mouse.

With reduced motion

The tram does not run, lean or throw sparks: choosing another link it fades out where it was and appears again, at rest, under the new link. The wire stays still and the doors shut; the words change weight without animation.

Pitfalls

  • The room between the wire and the roof is a few pixels: with a bigger tram the pantograph became a squashed dash. TRAM_SIZE is kept between 0.85 and 1.05, and the pantograph's foot is worked out from the tilted roof, so its head always stays on the wire.
  • The tram is double-ended, the same both ways: instead of turning it like a boat, the cream front lamp and the red tail lamp swap, the driver moves to the other cab and the pantograph trails on the right side.
  • Sparks drawn as circles looked like the glowing dots Luca does not want: they are short lines along the direction they fly, with gravity and a bounce on the rail.

Libraries

GSAP 3.15.0, Bricolage Grotesque (font variabile, con gli assi del peso e della larghezza) 5.3.0.

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

Testing

Tested on 23 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 components