Skip to content
animorio
IT
Back to the library

The ferry timetable

Hover, TextGSAP, CSS onlyMediumCompressed weight: Demo 9.2 KBGSAP 28 KBFreeDownload the zip

The winter timetable of a small ferry in thin tabular figures: under the pointer two vermilion hairlines stretch like rubber bands to the edges, one along the row and one down the column, the time where they cross takes weight and width, and the headers of the port and the boat snap. Minimal style.

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 ferry timetable</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/archivo@5.3.0/wdth.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage" data-stage>
    <header class="top">
      <span class="top__name"><span>Larkspit</span></span>
      <span class="top__note"><span>Winter timetable, every day</span></span>
    </header>

    <!-- the timetable: a port on every row, a boat on every column, a departure time in every cell. Under the pointer
         two hairlines stretch like rubber bands to the edges, one along the row and one down the column; the time
         where they cross takes weight and width, and the two headers snap -->
    <div class="board" data-board>
      <svg class="cross" data-cross aria-hidden="true">
        <path class="cross__line" data-line-row d="" />
        <path class="cross__line" data-line-col d="" />
      </svg>
      <div class="table" role="table" aria-label="Larkspit ferry, winter departures" tabindex="0" data-table></div>
      <i class="rule rule--head" data-rule aria-hidden="true"></i>
      <i class="rule rule--foot" data-rule aria-hidden="true"></i>
    </div>

    <footer class="foot">
      <span class="hint" data-hint>Point at a sailing</span>
      <span class="sr" aria-live="polite" data-say></span>
    </footer>
  </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.

20 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 ferry timetable” from Animorio and fit it into my site

What it does

On warm white paper, at the top the name of the line, Larkspit, and on the right Winter timetable, every day; in the middle a table with no frames: six boats in columns (Marta, Tern, Petrel, Osprey, Wren, Gannet), five ports in rows (from Larkspit to Rhee) and in every cell the departure time, in thin grey tabular figures of Archivo. As the page opens the two rules of the table draw from the left, the boats' names drop into place with a small bounce, the ports come in from the left and the times fill column after column, each from very thin to its weight; then the cross opens on a cell and takes two steps. Under the pointer two thin vermilion hairlines leave the point where it stands and stretch like rubber bands to the edges of the table, one along the row and one down the column; they run under the figures, so they show only between the cells, like the leaders of a printed timetable. Each line is held by the pointer and tied at its two ends: moving to another row the held point snaps there first and the ends arrive a moment later, so the line bends in a soft curve and straightens with a pixel too many; inside a cell the pointer pulls it a little towards itself. The time at the crossing takes weight and width on a spring that goes a hair past, the cells next to it feel the push and move two pixels away, the lit row and column turn from grey to black (the cells that go out leave afterwards, from the nearest to the farthest), and the names of the port and the boat snap towards the table and take weight. Leaving the table the lines go back into the pointer like rubber bands let go. When nobody points, after a moment the cross walks by itself from cell to cell. The arrow keys move a cell, Home and End go to the ends of the row.

How it works

A table in tabular figures

The table is a CSS grid with role=table: the first column for the ports, then a 1fr column for each boat, the rows with display: contents. The times are worked out from the boat's departure plus the port's minutes. Every text is a span with three CSS variables, --w (font-weight), --s (font-stretch) and --k (how much black is in the color-mix with the grey): GSAP animates only those, and Archivo's figures with tabular-nums never make the columns dance.

The lines under the figures

The two lines are two paths in an SVG that sits behind the grid. Every text has a background the colour of the paper with a little padding: the line runs under it and disappears behind the figures, so it shows only between the cells. No masks and no maths: the order of the layers is enough.

A rubber band held at one point

Each line is a path of two cubic curves: from the left end to the held point (where the pointer is along the line) and from there to the right end, with a flat tangent at the held point. The held point chases the middle of the row on a stiff spring (GRAB_SPRING), the ends on a softer one (ENDS_SPRING): changing row the line bends and straightens. PULL moves the held point towards the pointer inside the cell. The springs are integrated by hand on GSAP's ticker with the step capped at 1/30 of a second.

It stretches to the edges

A third number, reach, goes from 0 to 1 on a spring that is almost fully damped (it goes a pixel or two past): the ends stand at that fraction of the way between the pointer and the edges. When the cross goes out, reach goes back to 0 on a harder spring and the lines go back into the pointer. A new cross is born already on its row, with no bend.

Crossing, neighbours and headers

Changing cell, every text that changes state gets one tween only (the old one is stopped first, so a late one never undoes the new one): the crossing goes to WEIGHT[1] and WIDTH[1] with back.out, the row and the column to black, the others to grey with a delay that grows with the distance. The four neighbours make a push of NUDGE pixels and come back with elastic.out; the port and the boat snap 5 pixels towards the table and come back on a spring, taking weight.

Values to change

NameFileDefaultWhat it changes
GRAB_SPRINGscript.js[420, 30]The spring of the point where the pointer holds each line: how stiff, and how much it is held back.
ENDS_SPRINGscript.js[150, 17]The spring of the two ends of each line, which follow the held point a moment later (softer, more bend).
PULLscript.js0.22How much the pointer pulls the line off the middle of its row (0 = not at all).
WEIGHTscript.js[230, 680]The weight of the times at rest and of the time at the crossing (100 to 900).
WIDTHscript.js[88, 114]The width of the times at rest and of the time at the crossing (62 to 125).
NUDGEscript.js2How many pixels the cells next to the crossing move away.
WANDERscript.jstrueThe cross walks by itself when nobody points.

Try them in the Playground

When to use it

For every two-way table where you look for a crossing: ferry, train and bus timetables, the programme of a festival (stages and hours), the sizes of a shop, the prices of a hotel by season and room. It works while the table fits a screen, up to eight rows and eight columns; bigger than that, it is better filtered first.

On phones

The same animations with the sizes adapted: the figures are smaller and the rows taller, so the table fills the screen and the cells are easy to touch. The cross goes where you touch and follows the finger while it slides; when the finger lifts it stays there, and after a few seconds it starts walking by itself again. The page still scrolls up and down.

With reduced motion

No springs, no bends and no snaps: the lines appear already stretched on the row and the column, the crossing, the row and the headers change weight and colour with a short fade, the neighbours do not move and the cross does not walk by itself. When the page opens everything fades in already in place.

Pitfalls

  • A line through the middle of the row looks like it crosses the times out, like a cancelled sailing. It sits behind the figures and every text has a background the colour of the paper: so it shows only between the cells.
  • With one tween per cell and different delays, sweeping fast over three rows a cell could get the light first and then a fade started later, and stayed grey in the lit row. Every text keeps its last tween and stops it before making another.
  • With a round cap, the line ending right where the port's name begins left a speck to the left of the first letter: the name's background sticks out a little to the left with a negative margin.
  • The previews' pilot sends pointerleave events that bubble up from every element it leaves: the real exit is a pointerout with no relatedTarget and outside the window, and the cross goes out when the pointer leaves the table.

Libraries

GSAP 3.15.0, Archivo (font variabile) 5.3.0.

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

Testing

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