Skip to content
animorio
IT
Back to the library

Contents on a taut thread

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

A book's contents with a thin thread between each title and its page, instead of dots: under the pointer the thread goes taut and short, pulls the number across to the title and the number counts like a counter from the page you were at to the chapter's page, then lights up; the title takes weight. Let go, the number goes back to the margin and the thread slackens with a twang. 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>Contents on a taut thread</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/newsreader@5.3.0/standard.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage" data-stage>
    <header class="top">
      <span class="top__name">Low Water</span>
      <span class="top__note">Contents</span>
    </header>

    <!-- the contents: a title on the left, its page on the right, a hairline between them that hangs a little.
         Under the pointer the line goes taut and short, pulling the page towards the title while it counts up to it -->
    <nav class="toc" aria-label="Contents" data-toc></nav>

    <p class="hint">Point at a chapter</p>
  </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.

32 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 “Contents on a taut thread” from Animorio and fit it into my site

What it does

On warm white paper, at the top the book's title, Low Water, and on the right Contents; in the middle seven chapters in light Newsreader, from Before the tide to Walking home in the dark, each with its page in grey on the right margin and between the two a thin thread that hangs a little, like a real thread, instead of a row of dots. As the page opens the titles rise out of their mask one after the other, the threads draw themselves taut from the title towards the page while the numbers count from zero up to their page, and once there the threads slacken with a small swing. Pointing at a chapter, its thread turns black, goes taut and short: it pulls the number to just after the title, on a spring that goes a pixel past and comes back. Meanwhile the number counts like a counter from the page of the chapter you looked at before to this one's (from 41 to 121, or backwards), and when it gets there it lights up in vermilion; the title goes from weight 320 to 580. When the number lands the short thread gives a small ping and the threads of the chapters next door barely feel it. Leaving the chapter the number goes back to the margin, the thread grows long again and slackens with a twang like a plucked string, and the neighbours swing a little. A fast sweep over several chapters leaves a trail of numbers going out and back. The Tab key goes from chapter to chapter with the same effect.

How it works

A row, three pieces

Each chapter is a link with display: grid: the title and the number sit in the same cell with align-items: baseline, so their baselines match, and the number moves only with transform: translateX from the left edge. The thread is an absolute svg as big as the row, with a single path. In front of the number there is a pad of hidden zeros: while it counts it stays as wide as its page, so at rest it sits flush with the margin and on its way it never jumps.

The thread is one curve

Every frame the path is M x0 y Q xm y+s x1 y: it starts just after the title (its width is read again because it changes with the weight) and ends just before the number. The middle point drops by s, the thread spring's slack times the length: a long thread hangs SAG pixels, a short one almost nothing. The thread runs at the height of the middle of the figures.

Two springs per row

The number's position chases its goal (the margin, or the title plus GAP) on a nearly critical spring, NUMBER_SPRING, that goes a pixel past and comes back. The thread's slack chases zero when it is taut and SAG when it is slack, on a lightly damped spring, STRING_SPRING: that is why, let go, it twangs like a string. The springs are integrated by hand on GSAP's ticker in small steps of 1/240 of a second, steady at any screen rate.

The counter

The number is an object { v } carried by a GSAP tween, COUNT_TIME seconds on power2.out, from the page of the last chapter looked at to this one's, and written rounded every frame. At the end of the tween the row gets the is-set class and the number turns vermilion with a CSS transition. If you leave the chapter earlier, the count finishes quickly on its page while the number goes back.

Hands, neighbours and keys

A single pointermove on the window finds the row under the pointer from its band of height, as wide as the whole page: changing row lets the old one go and pulls the new one. Letting go and landing give a small kick to the slack speed of the threads next door, times NEIGHBOURS. A finger pulls the chapter it touches and leaves it pulled until it touches somewhere else; the links take the focus with Tab and do the same.

Values to change

NameFileDefaultWhat it changes
CHAPTERSscript.js[['Before the tide', 9], …]The chapters: the title and the page, in the book's order.
GAPscript.js68How close the number comes to its title, in pixels (the taut thread fills the space).
SAGscript.js3How far a long thread hangs at rest, in pixels.
NUMBER_SPRINGscript.js[230, 28]The number's spring: how stiff, and how much it is held back.
STRING_SPRINGscript.js[520, 7]The thread's spring: how stiff, and how much it is held back (less holds, longer twang).
WEIGHTscript.js[320, 580]The title's weight at rest and under the pointer (200 to 800).
COUNT_TIMEscript.js0.6The seconds the number takes to count up to its page.
NEIGHBOURSscript.js0.5How much the threads next door feel a number landing or going back (0 to 1).

Try them in the Playground

When to use it

For the contents of a book, an ebook or an online magazine, for the outline of a long article or guide, for the chapter list of a course: each entry with a number (page, minute, price) to tie to its title. It works with ten entries at most and titles that fit on one line; with long entries that wrap the thread has no room.

On phones

The same animations with the sizes adapted: the column is as wide as the screen, the titles smaller, the threads shorter and so hanging a little less. Touching a chapter the thread pulls it and leaves it pulled until you touch another chapter or somewhere else; dragging the finger goes from chapter to chapter. The hint at the bottom says Tap a chapter.

With reduced motion

No springs, no twanging threads and no count: under the pointer the number appears already next to the title with a short fade, with the thread short and taut, and the title takes weight; leaving, the number fades back in on the margin. When the page opens everything fades in already in place.

Pitfalls

  • At first the thread ran at the height of the capitals: the digits' offsetTop was not measured from the row. The right height comes from the difference between the getBoundingClientRect of the row and of the digits.
  • Counting from 121 down to 9 the number changes width at each digit lost and, resting on the margin, it would jump. In front of it there is a pad of zeros with visibility: hidden (Newsreader has no figure space), so it stays as wide as its page.
  • With 26 pixels between title and number the taut thread vanished, cut down to a speck between the two inner margins: the effect could not be read. With 68 pixels a short taut stroke stays visible.
  • The thread's spring is stiff (520): integrated with one step per frame, on a slow screen or in a long frame it becomes unstable. It is integrated in small steps of 1/240 of a second inside each frame.
  • With the same slack for all, the short threads (the taut one, the phone's) would bend like smiles: the slack is multiplied by the thread's length.
  • The previews' pilot sends pointerleave events that bubble from every element it leaves: the real exit from the page is a pointerout with no relatedTarget and outside the window.

Libraries

GSAP 3.15.0, Newsreader (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 6.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