
The cookie banner
Consent, Transitions, SVG, Physics
NewFree
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.
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>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
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 siteEach 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.
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 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.
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.
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.
| Name | File | Default | What it changes |
|---|---|---|---|
SPEED | script.js | 360 | The car’s top speed, in map units a second (the map is 360 wide). |
PICKUP | script.js | 760 | How quickly it picks up speed. |
BRAKE | script.js | 640 | How hard it brakes before a stop. |
GRIP | script.js | 1300 | How much sideways pull it takes in a curve: lower, it slows down more. |
TURNBACK | script.js | 0.3 | The seconds it waits where it has to change direction. |
SWAY | script.js | 1 | How much the body sways and the shadow moves in the curves. |
DRAW | script.js | 1.6 | The seconds the line takes to draw itself on arrival. |
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.
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.
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.
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.
Tested on 4 October 2026
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.
With your account’s email and password.
or with your email
You’re signed in as .
With your account, you can download the zip of every animation and every pack, for free.