
The cookie banner
Consent, Transitions, SVG, Physics
NewFree
The boarding pass of a mountain cable car, with the line drawn at its head: two cabins on their track cables, yours red and the other blue. The pass answers every change of status: for Boarding now the amber band comes in, the door opens, the lights come on and the code opens under a passing light; once it leaves, the cabin runs along the cable, which sinks under it, and really swings on its hanger, back as it leaves, with a knock on the saddle of the tower and forward as it brakes. Brush it with a finger or the pointer and the cabin sways.
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 cable car boarding pass</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/encode-sans@5.3.0/wdth.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the boarding pass of a cable car up a mountain, as the app that sold it shows it. Its head is the line itself:
the two cabins hang from their track cables, the cables sink under them, and each cabin swings on its hanger
with every push the ride gives it, at the stations and over the tower. The cables, the cabins and the code
are drawn and moved by the script -->
<section class="pass" aria-label="Cable car boarding pass" data-pass>
<article class="card" data-card>
<!-- the live part of the pass: the line, what is happening now, and where it goes -->
<div class="card__live" data-live-part>
<div class="scene" data-scene>
<p class="brand">
<svg class="brand__mark" viewBox="0 0 24 24" aria-hidden="true">
<path d="M2 21.5L8.5 14l3 3.8L15.5 12L22 21.5"/>
<path d="M2.5 6.8L21.5 2.6M10.8 5v2.4"/>
<rect class="brand__cab" x="7.6" y="7.4" width="6.4" height="5" rx="1.4"/>
</svg>
<span class="brand__text"><span class="brand__name">Morrow Peak Cableway</span><span class="brand__kind">Boarding pass</span></span>
</p>
<svg class="scene__svg" viewBox="0 0 360 196" role="img" aria-label="The line: Cabin Red waits at Alder Gap" data-svg>
<defs>
<linearGradient id="cc-sky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" class="g-sky-top"/>
<stop offset="1" class="g-sky-low"/>
</linearGradient>
<radialGradient id="cc-haze" cx="0.84" cy="0.16" r="0.62">
<stop offset="0" class="g-haze" stop-opacity="0.5"/>
<stop offset="1" class="g-haze" stop-opacity="0"/>
</radialGradient>
<linearGradient id="cc-rock" x1="0" y1="96" x2="0" y2="196" gradientUnits="userSpaceOnUse">
<stop offset="0" class="g-rock"/>
<stop offset="1" class="g-card"/>
</linearGradient>
<linearGradient id="cc-warm" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" class="g-lit" stop-opacity="0.95"/>
<stop offset="1" class="g-lit" stop-opacity="0.6"/>
</linearGradient>
</defs>
<rect width="360" height="196" fill="url(#cc-sky)"/>
<rect width="360" height="196" fill="url(#cc-haze)"/>
<!-- the range behind, in the haze -->
<path class="far" d="M0 112L18 100L34 106L58 80L76 94L92 86L112 100L136 72L152 84L170 78L196 96L220 70L246 88L268 64L300 60L360 54V196H0Z"/>
<path class="far-snow" d="M52 86L58 80L66 87L61 85L56 89ZM130 78L136 72L143 79L138 77L134 81ZM214 76L220 70L228 78L222 76L218 79Z"/>
<!-- the mountain the line climbs, its snow and the larches at its foot -->
<path class="rock" fill="url(#cc-rock)" d="M0 196V186H112C128 186 140 178 152 166L170 154L190 146L210 140L230 130L248 118L262 104L276 90L298 84L314 58L328 24L338 6L350 18L360 24V196Z"/>
<path class="snow" d="M300 82L314 58L328 24L338 6L350 18L360 24V44L352 40L346 50L338 42L330 56L322 54L316 70L308 74Z"/>
<path class="snow snow--shade" d="M338 6L350 18L360 24V44L352 40L346 50L340 36Z"/>
<path class="snow" d="M240 124L248 118L262 104L258 112L252 116L246 124Z"/>
<g class="trees" aria-hidden="true">
<path d="M96 172L91.5 182H94L90 189H102L98 182H100.5Z"/>
<path d="M108 168L102.5 180H105.5L101 189H115L110.5 180H113.5Z"/>
<path d="M121 175L117.5 183H119.5L116.5 189H125.5L122.5 183H124.5Z"/>
<path d="M134 164L129 175H131.5L127.5 185H140.5L136.5 175H139Z"/>
<path d="M146 158L141.5 168H144L140.5 177H151.5L148 168H150.5Z"/>
<path d="M159 150L155 159H157.5L154 167H164L160.5 159H163Z"/>
</g>
<!-- the two stations, inside: the platform each cabin docks at, and the light that comes on to board -->
<g class="station">
<rect class="station__wall" x="0" y="118" width="86" height="72"/>
<rect class="station__hall" x="40" y="124" width="46" height="48"/>
<rect class="station__light" x="40" y="124" width="46" height="48" fill="url(#cc-warm)" opacity="0" data-light="0"/>
<rect class="station__floor" x="40" y="172" width="46" height="3"/>
<path class="station__glass" d="M8 136h24v10H8z"/>
<path class="station__frame" d="M16 136v10M24 136v10"/>
</g>
<g class="station">
<rect class="station__wall" x="276" y="16" width="84" height="74"/>
<rect class="station__hall" x="276" y="22" width="44" height="50"/>
<rect class="station__light" x="276" y="22" width="44" height="50" fill="url(#cc-warm)" opacity="0" data-light="1"/>
<rect class="station__floor" x="276" y="72" width="44" height="3"/>
<path class="station__glass" d="M330 36h22v10h-22z"/>
<path class="station__frame" d="M337.5 36v10M344.5 36v10"/>
</g>
<!-- the far track: its cable and Cabin Blue, coming the other way -->
<path class="cable cable--far" data-cable="far"/>
<g class="cabin cabin--blue" data-cabin="far"></g>
<!-- the tower, with the saddles the cables run over -->
<g class="tower">
<path d="M138.0 166L143.6 105.6M154.0 166L148.4 105.6M138.0 166L152.7 152M154.0 166L139.3 152M139.3 152L151.5 139M152.7 152L140.5 139M140.5 139L150.4 127M151.5 139L141.6 127M141.6 127L149.4 116M150.4 127L142.6 116M142.6 116L148.6 108M149.4 116L143.4 108"/>
<path class="tower__arm" d="M136 105.6H156"/>
</g>
<!-- the near track: its cable and your cabin -->
<path class="cable" data-cable="near"/>
<g class="cabin cabin--red" data-cabin="near"></g>
<!-- the roofs over the platforms, in front of the cabins -->
<path class="station__roof" d="M-2 121L90 110V116.5L-2 126.5Z"/>
<path class="station__roof" d="M270 17L362 7V13L270 23Z"/>
</svg>
</div>
<!-- what is happening now: the script writes it, and the line under the card says it aloud -->
<div class="status" data-status>
<span class="status__band" aria-hidden="true" data-band></span>
<span class="status__icon" aria-hidden="true" data-status-icon></span>
<span class="status__text">
<span class="status__title" data-status-title>Boarding at 09:40</span>
<span class="status__sub" data-status-sub>Cabin Red is at Alder Gap</span>
</span>
</div>
<div class="route" data-route>
<p class="stop">
<span class="stop__label">From</span>
<span class="stop__move" data-stop="0">
<span class="stop__name" data-name="0">Alder Gap</span>
<span class="stop__alt" data-alt="0">1,046 m</span>
</span>
</p>
<p class="stop">
<span class="stop__label">To</span>
<span class="stop__move" data-stop="1">
<span class="stop__name" data-name="1">Morrow Peak</span>
<span class="stop__alt" data-alt="1">2,610 m</span>
</span>
</p>
</div>
</div>
<dl class="facts">
<div class="fact"><dt>Boarding</dt><dd data-board>09:40</dd></div>
<div class="fact"><dt>Departs</dt><dd data-depart>09:45</dd></div>
<div class="fact"><dt>Cabin</dt><dd>Red</dd></div>
<div class="fact"><dt>Gate</dt><dd data-gate>B</dd></div>
</dl>
<div class="ticket">
<div class="code">
<svg class="code__svg" viewBox="-2 -2 29 29" role="img" aria-label="Boarding code for ticket MPC 4471 2093" data-code></svg>
</div>
<div class="holder">
<dl class="holder__list">
<div><dt>Passenger</dt><dd>Mara Lindqvist</dd></div>
<div><dt>Ticket</dt><dd>Adult, return</dd></div>
</dl>
<p class="holder__note" data-code-note>Code opens at 09:40</p>
</div>
</div>
</article>
<div class="bar">
<p class="bar__updated" data-updated>Updated 09:31</p>
<button class="act" type="button" data-act data-autoclic>
<span class="act__icon" aria-hidden="true" data-act-icon></span>
<span class="act__label" data-act-label>Refresh</span>
</button>
</div>
<p class="sr" aria-live="polite" data-live></p>
</section>
</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.
69 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 cable car boarding pass” from Animorio and fit it into my siteEach track cable goes from the valley station to the saddle of the tower and from there to the top station, in two spans. In a span the cable is a taut string: under its own weight it makes a parabola, y = chord + s·4u(1−u), with the sag s growing as the square of the span; with the cabin's load at c it adds a triangle, d·4·u(1−c) before the load and d·4·c(1−u) after it, with d growing with the span. So under the carriage the cable has a corner, and it is redrawn every frame as a path with a point every 5 pixels, plus the exact ones at the tower and at the load.
The carriage always sits on the cable bent by its own weight: its way is the parabola with the whole sag, s + d. To make it run at an even speed along the cable and not along the ground, the way is measured once in 480 pieces and x is found from the length with a binary search. The ride follows the profile of a real drive: constant acceleration, constant speed, constant braking (a trapezoid of speed, RAMP 0.32). The other cabin is tied to the same haul rope: it is always at 1 − p.
The cabin hangs from the carriage. For a pendulum hanging from a point that accelerates by (ax, ay), with y pointing down: φ'' = (−(g − ay)·sin φ − ax·cos φ)/L − 2ζωφ'. With g = ω²L, where ω comes from SWING. The point's acceleration is not guessed from the frames: it is the second difference of the carriage's exact position at t − h, t, t + h, with h = 1/240 s, and the pendulum moves in fixed steps of 1/240 with an accumulator. That is why at the tower's corner, where the slope changes at once, the cabin always gets the same knock, at any frame rate.
Inside a station the cabin is between the guides: the damping rises to 0.45 as it goes into the station (over a 14 pixel ramp) and the angle has a soft limit of about 9 degrees, past which a strong spring pushes it back: it bumps gently against the guide instead of going through the wall. Pushes from the finger or the pointer are worth a third. When both cabins are still and not running, the loop on GSAP's ticker switches off.
The pointer passing close to a cabin (less than 30 pixels from its middle, worked out from the angle) gives it an angular push in proportion to how far it moved, not to its speed, so it does not depend on how many events the browser sends; a tap pushes it away from the finger. The head has touch-action pan-y, so a finger can brush the cabin and the page still scrolls up and down.
There are four states, Boarding at, Boarding now, On your way, Arrived, then the return. The amber band comes in with a clip-path inset from the left and the new words only arrive once it is there (and on the way out it leaves before them), so white words are never seen on the amber. The code is a drawing made like a QR code (three squares, the timing lines, the alignment square and modules worked out from the ticket number) in two layers, off and on: the one that is on is cut by a polygon x + y < k, and a blurred strip of light follows the edge while k goes from −6 to 62. The stations' names change places with a FLIP: the one going up passes in front and the one going down behind, and it is gone before they overlap.
| Name | File | Default | What it changes |
|---|---|---|---|
RIDE | script.js | 3.4 | The seconds of a ride from one station to the other (the real one takes eight minutes). |
SWING | script.js | 1.3 | The seconds of one swing of the cabin on its hanger: longer, and it leans more as it speeds up. |
DAMPING | script.js | 0.06 | How soon a swing dies away in the air (1 = it does not sway). |
SAG | script.js | 7 | How far the cable sinks under the cabin in the middle of a span 100 pixels long: the more it sinks, the harder the knock at the tower. |
NUDGE | script.js | 1 | How much a cabin answers a finger or the pointer brushing past (0 = not at all). |
UP | script.js | ['09:40', '09:45', '09:53'] | The ride up: boarding, departure and arrival. |
DOWN | script.js | ['16:20', '16:25', '16:33'] | The ride down, on the return half of the ticket. |
GATES | script.js | ['B', 'A'] | The gate at the valley station and at the top. |
LAST_DOWN | script.js | '17:30' | The last cabin down, said on arrival at the top. |
For the app of a cable car, a rack railway, a ferry or a night train; for the pass of a sightseeing ride with a boarding time; for a wallet card that follows the status of the trip; anywhere a ticket can show where the vehicle really is.
The pass is 360 pixels wide, 358 on a 390 screen with its 16 pixel margins; the drawing of the line scales with its viewBox, and on screens narrower than 360 the names and the code get a little smaller. A finger presses the button and everything goes as with the mouse; touching a cabin pushes it, and brushing it sideways makes it sway, while the page still scrolls up and down.
Nothing moves: the cabins neither swing nor travel, they fade out where they are and fade in where they arrive; the door is open or shut at once, the amber band and the code come in with a fade, the names and the times change with a fade, the button does not stretch and the arrow does not turn. The cables and the cabins are already in place as it comes in.
GSAP 3.15.0, Encode Sans (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 16 KB; the libraries: GSAP 28 KB.
Tested on 7 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.