Skip to content
animorio
IT
All components

The metro map menu

Navigation, SVG, Physics, TransitionsGSAPAdvancedCompressed weight: Demo 12 KBGSAP 28 KBFreeDownload the zip

A menu as a metro line: the links are the stops, and picking one sends a little car there along the track, slowing into the curves and swaying, while the line takes its colour behind it; from an interchange a branch leads to the sub-pages.

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 metro map menu</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/overpass@5.3.0/index.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/doto@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <div class="layout" data-layout>
      <!-- the menu: a metro line. The stops are links laid over the drawing; a little car rides the line to the stop
           chosen, and the line takes its colour behind it. Venues is an interchange: a branch leads to its halls -->
      <nav class="map" aria-label="Main" data-map>
        <svg class="map__art" viewBox="0 0 360 540" aria-hidden="true" data-art>
          <defs>
            <radialGradient id="beam" cx="0" cy="0.5" r="1" fx="0" fy="0.5">
              <stop offset="0" stop-color="#fff3c4" stop-opacity="0.95" />
              <stop offset="1" stop-color="#fff3c4" stop-opacity="0" />
            </radialGradient>
          </defs>
          <g data-tracks></g>
          <g data-ghost></g>
          <g data-paint></g>
          <path class="route" data-route />
          <g data-marks></g>
          <!-- the car, seen from above: the end with the white lamps is the one it travels towards -->
          <!-- the shadow: three soft layers instead of a blur filter, which would be redrawn at every frame -->
          <g class="car__shadow" data-shadow>
            <rect x="-23" y="-10.5" width="46" height="21" rx="10" />
            <rect x="-21.5" y="-9" width="43" height="18" rx="8.5" />
            <rect x="-20" y="-7.5" width="40" height="15" rx="7" />
          </g>
          <g class="car" data-car>
            <g data-body>
              <ellipse class="car__beam" cx="34" cy="0" rx="16" ry="8" fill="url(#beam)" data-beam />
              <rect class="car__body" x="-20" y="-7.5" width="40" height="15" rx="7" />
              <rect class="car__roof" x="-12.5" y="-4.6" width="25" height="9.2" rx="2.4" />
              <rect class="car__stripe" x="-12.5" y="-1.5" width="25" height="3" rx="1.5" />
              <path class="car__glass" d="M14 -5.4h1.6a3.6 3.6 0 0 1 3.4 3.4v4a3.6 3.6 0 0 1-3.4 3.4H14z" />
              <path class="car__glass" d="M-14 -5.4h-1.6a3.6 3.6 0 0 0-3.4 3.4v4a3.6 3.6 0 0 0 3.4 3.4H-14z" />
              <circle class="car__lamp car__lamp--a" cx="18.6" cy="-5" r="1.25" />
              <circle class="car__lamp car__lamp--a" cx="18.6" cy="5" r="1.25" />
              <circle class="car__lamp car__lamp--b" cx="-18.6" cy="-5" r="1.25" />
              <circle class="car__lamp car__lamp--b" cx="-18.6" cy="5" r="1.25" />
            </g>
          </g>
        </svg>

        <ol class="map__stops">
          <li><a class="stop" href="#" data-stop="welcome" aria-current="page">Welcome</a></li>
          <li><a class="stop" href="#" data-stop="programme">Programme</a></li>
          <li><a class="stop" href="#" data-stop="tours">Tours</a></li>
          <li>
            <a class="stop stop--change" href="#" data-stop="venues">Venues</a>
            <ol class="map__branch" aria-label="Venues">
              <li><a class="stop" href="#" data-stop="docks">Old Docks</a></li>
              <li><a class="stop" href="#" data-stop="glasshouse" data-autoclic>Glasshouse</a></li>
              <li><a class="stop" href="#" data-stop="depot">Tram Depot</a></li>
            </ol>
          </li>
          <li><a class="stop" href="#" data-stop="tickets" data-autoclic>Tickets</a></li>
        </ol>

      </nav>

      <!-- the station board: the stop the car is at, or the one it is going to while it travels -->
      <section class="board" aria-labelledby="board-title" data-board>
        <p class="board__status">
          <span class="board__state" data-status>Now at</span>
          <span class="board__line" data-line>Canal line</span>
        </p>
        <h1 class="board__title" id="board-title"><span class="sr" data-title-name>Welcome</span><span aria-hidden="true" data-title>Welcome</span></h1>
        <p class="sr" aria-live="polite" data-live></p>
      </section>
    </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.

28 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 metro map menu” from Animorio and fit it into my site

What it does

On a white background a metro line, blue, with the stops Welcome, Programme, Tours, Venues and Tickets; from Venues, an interchange, the orange branch leaves with Old Docks, Glasshouse and Tram Depot. Beside it, a dark station board says Now at in green, the name of the line and the stop’s name in glowing dots. On arrival the line draws itself from the top and the stops pop up as the drawing reaches them. Over a stop, a marching dashed line shows the way the car will take. Picking it, the car sets off, picks up speed, slows before the curves and leans into them swaying, with its shadow moving outwards, and behind it the line takes its colour; the stops it passes give a little jolt. The board says Next stop in amber and the letters of the name roll through the alphabet until they settle. On arrival it brakes, the stop gives a little bounce and the board goes back to Now at. To go from the line below the interchange onto the branch, the car stops at the interchange, switches its lamps and sets off backwards onto the branch, like real trains.

How it works

The track in pieces

Each line is a row of points; the corners are rounded with a radius. The script turns it into straight pieces and arcs, each with its length and its curvature (zero or 1/radius), and writes the same track as an SVG path with L and A commands. So for any distance along the line it knows the point, the heading and how sharply it bends, with no getPointAtLength.

The trip and the speed limit

A trip is made of legs: along one line, or to the interchange and then onto the other. Before leaving, the script works out a speed limit for every unit of the way: SPEED on the straight, √(GRIP / curvature) in a curve, zero where the car must change direction and at the end; then, walking backwards, the limit may only grow as fast as braking allows (v² = v₀² + 2·BRAKE·d). Every frame the car speeds up by PICKUP towards the limit, with the step capped at 1/30 of a second.

The sway

The sideways pull is v² times the curvature: it drives a spring on the body’s angle, integrated in steps of 1/240 of a second, which lags going into a curve and overshoots a little coming out. The shadow is three see-through rectangles on top of each other (no blur filter, which would be worked out again every frame) and it moves towards the outside of the curve.

The line taking its colour

Every trip lays its own paint: one path per leg, with a pathLength equal to its length and a stroke-dasharray uncovered with stroke-dashoffset as the car moves on. The dash’s gap is longer than the line, so a line not yet uncovered does not even show the dot of its round cap. At the next departure the old paint fades.

Stops, board and preview

The stops are links over the drawing, placed in map units with container units (100cqi / 360), so they grow with it. The preview route is a thin dashed path (stroke-dasharray 5 4) marching with a CSS animation. The board’s state is just a word in its colour: Now at in green, Next stop in amber. The board’s letters, in Doto, roll at random and settle from left to right with a GSAP tween.

Values to change

NameFileDefaultWhat it changes
SPEEDscript.js360The car’s top speed, in map units a second (the map is 360 wide).
PICKUPscript.js760How quickly it picks up speed.
BRAKEscript.js640How hard it brakes before a stop.
GRIPscript.js1300How much sideways pull it takes in a curve: lower, it slows down more.
TURNBACKscript.js0.3The seconds it waits where it has to change direction.
SWAYscript.js1How much the body sways and the shadow moves in the curves.
DRAWscript.js1.6The seconds the line takes to draw itself on arrival.

Try them in the Playground

When to use it

For the site of a festival, a city, a museum with several buildings, a conference with several rooms, or a guide’s index: wherever the pages really are stages along a route and a group of sub-pages can sit on a branch. From five to ten stops.

On phones

The board goes on top and the map below, as wide as the screen up to 420 pixels; the stops grow and shrink with it and stay big enough for a finger. Without a pointer there is no preview route: a tap sends the car at once.

With reduced motion

The car does not travel: it fades out and reappears at the chosen stop, while the way it took fades in coloured. The line does not draw itself on arrival, the board’s letters do not roll, the route’s dashes do not march and the stops do not jolt.

Pitfalls

  • With getPointAtLength and a tween on the length the car went at a steady speed even through tight curves; building the track in pieces gives the exact curvature, and from it come the limit in the curves and the sway.
  • A speed limit worked out only forwards brakes too late: it is fixed by walking backwards from the end, because every point must be able to stop in time for all those after it.
  • Near the stop the car went slower and slower without ever arriving: under a minimum speed (9 units a second) it finishes the run.
  • When changing direction the body turned round suddenly halfway through the wait: the car moves a hair onto the new leg as soon as it stops, so it waits already facing the new way and only the lamps change (white ahead, red behind).
  • A feGaussianBlur filter on the shadow, moved every frame, made the blur be worked out over and over and slowed browsers without a graphics card a lot: three see-through rectangles make a soft shadow at no cost.
  • With dash and gap the same length, a line with round caps not yet uncovered leaves a dot at its start: the gap must be longer than the line.

Libraries

GSAP 3.15.0, Overpass (font variabile) 5.3.0, Doto (font variabile) 5.3.0.

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

Testing

Tested on 4 October 2026

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

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