Skip to content
animorio
IT
All components

The cable car boarding pass

Tickets, Physics, SVG, TransitionsGSAPAdvancedCompressed weight: Demo 16 KBGSAP 28 KBFreeDownload the zip

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.

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 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>

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.

69 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 cable car boarding pass” from Animorio and fit it into my site

What it does

On a white background the boarding pass of Morrow Peak Cableway, in a deep mountain blue, as the app that sold it shows it. At its head is the line itself, in a poster style: the morning sky, the peaks in the haze, the mountain with its snow, the larches, the valley station and the top station in pale concrete, a tower, and the two track cables with their cabins: yours, Cabin Red, waiting at the Alder Gap station, and Cabin Blue at the top. Below, the status, Boarding at 09:40 with Cabin Red is at Alder Gap; From Alder Gap 1,046 m, To Morrow Peak 2,610 m in a narrow type; Boarding, Departs, Cabin and Gate; the code to show, not open yet, with Code opens at 09:40, the passenger's name and the ticket. Under the pass Updated 09:31 and the Refresh button. As it comes in the cables run out from the valley and the cabins arrive swinging. Refresh turns its arrow and brings the new status: Boarding now, on an amber band that comes in from the left; the red cabin's door slides into the wall, the lights of the cabins and the station come on, the cabin gives a little as people step in, and a warm light passes diagonally over the code and opens it module by module, with Show it at Gate B. At the next Refresh the band leaves, the door closes, the code goes out with Used at Gate B, 09:43 and the status says On your way up, Departed 09:45, arrives 09:53. The two cabins leave together, tied to the same haul rope, one going up and the other down: the cable sinks under each cabin with a corner where the carriage holds it, the cabin leans back as it speeds up, gets a knock and swings as it rolls over the tower, passes the other one halfway, and swings forward as it brakes until the station's guides calm it. Once it is in, the door opens and the status says Arrived at Morrow Peak, 09:53, last cabin down at 17:30; the button becomes Show return pass. Pressed, the pass turns to the return half: the two names change places like the cabins, the one going up passing in front and the one going down passing behind as it fades, the times roll to 16:20 and 16:25, the gate becomes A, and it all starts again from the top. Moving the pointer close to a cabin, or touching it, pushes it and it swings; in a station much less, because the guides hold it.

How it works

The cable: a taut string with a weight

Each 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 runs on the curve it makes

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 is a pendulum on a moving point

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.

The station's guides

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 finger that pushes it

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.

The pass that follows the trip

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.

Values to change

NameFileDefaultWhat it changes
RIDEscript.js3.4The seconds of a ride from one station to the other (the real one takes eight minutes).
SWINGscript.js1.3The seconds of one swing of the cabin on its hanger: longer, and it leans more as it speeds up.
DAMPINGscript.js0.06How soon a swing dies away in the air (1 = it does not sway).
SAGscript.js7How 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.
NUDGEscript.js1How much a cabin answers a finger or the pointer brushing past (0 = not at all).
UPscript.js['09:40', '09:45', '09:53']The ride up: boarding, departure and arrival.
DOWNscript.js['16:20', '16:25', '16:33']The ride down, on the return half of the ticket.
GATESscript.js['B', 'A']The gate at the valley station and at the top.
LAST_DOWNscript.js'17:30'The last cabin down, said on arrival at the top.

Try them in the Playground

When to use it

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.

On phones

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.

With reduced motion

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.

Pitfalls

  • The two cabins pass each other halfway along the cable, and with the tower halfway they passed right on the tower, in a tangle of lattice and cabins: the tower moved to the exit of the valley station and the passing point is in the middle of the long span.
  • With the same sag for two spans of different lengths, the short one made a huge corner at the tower: the sag of the weight grows as the square of the span and that of the load as the span, as in a real cable.
  • The knock at the tower changed with the frame rate: the carriage's acceleration comes from its exact position at ±1/240 of a second and the pendulum moves in fixed steps, so the corner always gives the same push.
  • Pushed in a station, the cabin swung out through the wall: inside the station the damping rises, a push is worth a third and the angle has a soft limit, like real guides.
  • The door sliding over the windows looked like a red block: it is a pocket door, clipped to its doorway, that disappears into the wall.
  • The white words of the new status appeared over the amber band as it left: the band moves first and the words wait for it.
  • The two names changing places overlapped halfway: the one passing behind is gone before it reaches the other.
  • The strip of light on the code spilled out of the rounded corner and was jagged (the code has shape-rendering crispEdges): it sits in a clipPath of the paper, blurred, with geometricPrecision.
  • stop-color="var(--colour)" in the gradients' attributes does not work: presentation attributes do not read variables, so the gradients' colours sit in CSS classes.
  • The inline transform GSAP leaves on the button's icon after a change of words cancelled the hover rotation in the CSS: the change clears the transform, and the turn of Refresh moves the svg inside, not the span.
  • The preview's click every 2.8 seconds came with the ride already under way: during the ride the button is aria-disabled and a click does nothing.

Libraries

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.

Testing

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