
The bar that becomes an island
Navigation, Physics, Scroll, SVG, Transitions
NewFree
Scroll inside the preview
An article’s table of contents as on the best documentation sites: beside the entries runs a rail that follows the outline of the headings and steps in with a soft bend for every sub-heading, and its lit stretch covers exactly the part of the page on screen, its two ends on springs. A pressed entry glides the page there; on a phone the table of contents becomes a bar with the current section.
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 table of contents rail</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/schibsted-grotesk@5.3.0/index.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="layout" data-layout>
<!-- the table of contents: on a wide screen a column on the right that stays put, on a phone a bar at the top
that opens it. The rail beside the entries is an SVG path drawn from where the entries are; its lit stretch
covers the sections on screen -->
<aside class="toc" data-toc>
<button class="toc__bar" type="button" aria-expanded="false" aria-controls="toc-panel" data-bar>
<svg class="toc__ring" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="9" pathLength="1" data-ring/></svg>
<span class="toc__bar-words"><span class="toc__bar-label">On this page</span><span class="toc__bar-now" data-now>Overview</span></span>
<svg class="toc__chevron" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg>
</button>
<div class="toc__panel" id="toc-panel" data-panel>
<p class="toc__title" id="toc-title">On this page</p>
<nav class="toc__nav" aria-labelledby="toc-title">
<svg class="rail" aria-hidden="true" data-rail>
<path class="rail__track" data-track/>
<path class="rail__read" data-read/>
<path class="rail__ghost" data-ghost/>
<path class="rail__halo" data-halo/>
<path class="rail__lit" data-lit/>
<circle class="rail__head" r="3.5" data-head/>
</svg>
<ol class="toc__list" data-list>
<li><a href="#overview" data-depth="2">Overview</a></li>
<li><a href="#how-signing-works" data-depth="2">How signing works</a></li>
<li><a href="#the-signature-header" data-depth="3">The signature header</a></li>
<li><a href="#rotating-the-secret" data-depth="3">Rotating the secret</a></li>
<li><a href="#two-secrets-at-once" data-depth="4">Two secrets at once</a></li>
<li><a href="#verify-a-request" data-depth="2">Verify a request</a></li>
<li><a href="#with-the-sdk" data-depth="3">With the SDK</a></li>
<li><a href="#by-hand" data-depth="3">By hand</a></li>
<li><a href="#retries-and-replays" data-depth="2">Retries and replays</a></li>
<li><a href="#the-retry-schedule" data-depth="3">The retry schedule</a></li>
<li><a href="#rejecting-old-events" data-depth="3">Rejecting old events</a></li>
<li><a href="#test-locally" data-depth="2">Test locally</a></li>
<li><a href="#going-live" data-depth="2">Going live</a></li>
</ol>
</nav>
<div class="toc__foot">
<span class="toc__left" data-left>3 min read</span>
<button class="toc__up" type="button" data-up>
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 13V3M4 7l4-4 4 4"/></svg>
Back to top
</button>
</div>
</div>
</aside>
<!-- the article it indexes: plain headings and short paragraphs, only there to be read and scrolled -->
<main class="doc" data-doc>
<h2 id="overview" tabindex="-1">Overview</h2>
<p>A webhook endpoint is a public URL. Anyone who finds it can send it a request that looks exactly like a real event, and if your code acts on it, a stranger can mark orders as paid or cancel bookings.</p>
<p>A signature on every event closes that door: your code repeats the sender’s computation and acts only when the two results match.</p>
<h2 id="how-signing-works" tabindex="-1">How signing works</h2>
<p>Each endpoint has its own signing secret, shown once when the endpoint is created. The secret never travels with the events: only what it produces does.</p>
<p>A signature covers the moment the event was sent and its body, so a request captured on the way cannot be replayed hours later.</p>
<h3 id="the-signature-header" tabindex="-1">The signature header</h3>
<p>Every event carries a header with a timestamp and one or more signatures, separated by commas. Each signature is an HMAC-SHA256 of the timestamp, a full stop and the raw body.</p>
<h3 id="rotating-the-secret" tabindex="-1">Rotating the secret</h3>
<p>Rotate a secret when someone who knew it leaves the team, when it may have been logged by accident, or simply once a year, and choose how long the old one keeps working.</p>
<h4 id="two-secrets-at-once" tabindex="-1">Two secrets at once</h4>
<p>While both secrets are valid, every event is signed with each of them. Code that accepts any of the signatures keeps working through the change, without a single rejected event.</p>
<h2 id="verify-a-request" tabindex="-1">Verify a request</h2>
<p>Verification comes before anything else in your handler: before parsing, before logging the body, before touching the database. Anything that fails gets an error and no further work.</p>
<h3 id="with-the-sdk" tabindex="-1">With the SDK</h3>
<p>The official libraries do the whole check in one call, including the tolerance on the timestamp and the secrets in rotation, and throw an error when it fails.</p>
<h3 id="by-hand" tabindex="-1">By hand</h3>
<p>Without a library the check is short enough to write yourself. Compare the signatures in constant time, and check every one in the header, not just the first.</p>
<h2 id="retries-and-replays" tabindex="-1">Retries and replays</h2>
<p>An event counts as delivered when your endpoint answers with a success status within ten seconds. Anything else, a timeout included, is a failure, and the event is sent again later.</p>
<h3 id="the-retry-schedule" tabindex="-1">The retry schedule</h3>
<p>Failed events are tried again seven times over about a day and a half, waiting longer each time. Each new attempt is signed again with a fresh timestamp.</p>
<h3 id="rejecting-old-events" tabindex="-1">Rejecting old events</h3>
<p>Reject events whose timestamp is more than five minutes away from your server’s clock: a request recorded on the way is then useless almost as soon as it is captured.</p>
<h2 id="test-locally" tabindex="-1">Test locally</h2>
<p>The command line tool forwards real, signed events to a URL on your own machine, with a temporary secret for the session, so a failed check shows up at once.</p>
<h2 id="going-live" tabindex="-1">Going live</h2>
<p>Before switching to live mode, make sure the raw body is verified first, the live secret sits in your secret manager, old events are rejected and slow work goes to a queue.</p>
<p>From then on your endpoint acts only on events that are genuine.</p>
</main>
</div>
<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.
61 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 table of contents rail” from Animorio and fit it into my siteEach entry has a level (data-depth) and from it an x: INDENT pixels more for each level. The real positions of the entries (offsetTop and offsetHeight) give, for each entry, where it starts and ends. The rail is a function of height: at every y there is one x, the entry’s, and between two entries at different levels the x goes from one to the other along a smoothstep bend BEND pixels tall, centred on the line between them. The SVG path is made of points taken from this function: the two ends, and nine points along every bend.
Each section of the article runs from its heading to the next, and the last one to its last paragraph. The window (below the bar, on a phone) becomes two heights on the rail: of the first visible section the fraction already gone past the top edge, of the last the fraction that has reached the bottom edge, laid on their entries. The lit stretch is a second path, taken from the same function between those two heights: it follows the bends by itself.
The two ends of the stretch do not jump: each is a number with a speed, pulled towards its goal by a spring (SPRING) and damped. The end on the side you are going uses the full spring, the other a softer one (TRAIL) with the damping scaled to match, so it does not wobble: on a long jump the stretch pulls out and then gathers. The step is capped at 1/30 of a second and the path is only rewritten when one of the two heights has changed.
A pressed entry takes the page to its heading (minus the scroll-margin-top, which on a phone leaves room for the bar) with a GSAP tween on a number that calls scrollTo on every update: the duration grows with the distance up to GLIDE seconds. The wheel, a finger or the scrolling keys stop the trip. On arrival the heading takes the focus (it has tabindex=-1), the address takes its #, and the green colour fades away.
Under 900 pixels the table of contents becomes a bar stuck to the top of the screen: an SVG ring filled with stroke-dashoffset from the fraction read, and the name of the current section rolling up or down. Pressed, the list opens with a clip-path coming down and the entries arriving one after the other; the rail is measured again because the entries are now bigger. A chosen entry closes the list and glides the page.
| Name | File | Default | What it changes |
|---|---|---|---|
INDENT | script.js | 12 | The pixels the rail steps in for each level of heading. |
BEND | script.js | 14 | The height of each bend of the rail, in pixels. |
SPRING | script.js | [210, 24] | The two ends of the lit stretch: the strength of their spring and its damping. |
TRAIL | script.js | 0.35 | How much softer the spring of the trailing end is: lower, the stretch pulls out longer on jumps. |
GLIDE | script.js | 1.1 | The longest the page takes to glide to a section, in seconds. |
READ | script.js | 230 | Words read in a minute, for the minutes left. |
HALO | script.js | true | A soft glow round the lit stretch. |
For documentation, guides, long articles, terms of service: any page with headings and sub-headings where readers want to know where they are and jump to a point. With fewer than four headings it is not needed.
Under 900 pixels the table of contents becomes a bar stuck to the top of the screen, with a ring of how much you have read and the name of the current section; a tap opens the list with its rail, a tapped entry closes it and goes there, a tap outside closes. The article goes to one column as wide as the screen, and nothing spills over sideways.
The lit stretch follows the scroll without springs, an entry goes to its section at once without gliding, the name in the bar changes without rolling and the phone’s list fades open.
GSAP 3.15.0, Schibsted Grotesk (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 10 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.