
The funnel context menu
Menus, Transitions, SVG, Hover
NewFree
The announcements of a running club, where members can only react. Every reaction is drawn with one line, like the drawings runners trace on a map with their routes. Chosen from the bar, the drawing unravels into a line that runs a lap round the message like a GPS trace, draws the reaction again in its place under it and then zips in after it like a tape measure: the reaction lands with a jolt and its number rolls on. Tapped under the message, it runs the whole lap and comes back where it started; taken back, it unravels and runs off backwards.
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 reactions that run a lap</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">
<!-- the announcements of a running club: a reaction leaves the bar as a line, runs a lap round the message like a
GPS trace and zips into its place under it, drawing itself there; taken back, it unravels and runs off -->
<section class="chat" aria-label="Lockside Run Club, announcements" data-chat data-autoclic>
<header class="chat__head">
<svg class="chat__mark" viewBox="0 0 44 44" aria-hidden="true">
<rect class="mark__track" x="7" y="13.5" width="30" height="17" rx="8.5"/>
<rect class="mark__lane" x="12.5" y="18.5" width="19" height="7" rx="3.5"/>
<path class="mark__line" d="M22 13.5v5"/>
</svg>
<div class="chat__title">
<h2 class="chat__name">Lockside Run Club</h2>
<p class="chat__sub">Announcements · 46 members</p>
</div>
</header>
<div class="chat__thread" data-thread>
<div class="chat__content" data-content>
<ol class="chat__log" data-log>
<li class="msg" data-reactions="kudos 9, love 3" style="--face: #f0915f">
<div class="msg__row">
<span class="msg__face" aria-hidden="true">M</span>
<div class="bubble" data-bubble>
<p class="bubble__who">Maya</p>
<p class="bubble__text">Sunday long run: 18 km from the lock gates, 8:00 sharp.<span class="bubble__room" aria-hidden="true"></span></p>
<span class="bubble__time">07:12</span>
</div>
</div>
<div class="chips" role="group" aria-label="Reactions to Maya’s message" data-chips></div>
</li>
<li class="msg" data-reactions="kudos 6" style="--face: #6aa6ff">
<div class="msg__row">
<span class="msg__face" aria-hidden="true">J</span>
<div class="bubble" data-bubble>
<p class="bubble__who">Jonah</p>
<p class="bubble__text">Track night moves to Thursday, the stadium has a concert.<span class="bubble__room" aria-hidden="true"></span></p>
<span class="bubble__time">08:40</span>
</div>
</div>
<div class="chips" role="group" aria-label="Reactions to Jonah’s message" data-chips></div>
</li>
<li class="msg" data-reactions="fire 7, medal 4" style="--face: #c79bff">
<div class="msg__row">
<span class="msg__face" aria-hidden="true">P</span>
<div class="bubble" data-bubble>
<p class="bubble__who">Priya</p>
<p class="bubble__text">Saturday’s results: Dev ran the 10K in 38:41, a new club record!<span class="bubble__room" aria-hidden="true"></span></p>
<span class="bubble__time">18:05</span>
</div>
</div>
<div class="chips" role="group" aria-label="Reactions to Priya’s message" data-chips></div>
</li>
<li class="msg" data-reactions="haha 12" data-mine="haha" style="--face: #6aa6ff">
<div class="msg__row">
<span class="msg__face" aria-hidden="true">J</span>
<div class="bubble" data-bubble>
<p class="bubble__who">Jonah</p>
<p class="bubble__text">Lost and found: one left shoe, size 42. Smells heroic.<span class="bubble__room" aria-hidden="true"></span></p>
<span class="bubble__time">18:31</span>
</div>
</div>
<div class="chips" role="group" aria-label="Reactions to Jonah’s message" data-chips></div>
</li>
</ol>
<!-- the lines that run, above the messages -->
<svg class="chat__runs" aria-hidden="true" data-runs></svg>
</div>
</div>
<footer class="chat__foot">
<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="10.5" width="14" height="10" rx="2.5"/><path d="M8 10.5V8a4 4 0 0 1 8 0v2.5"/></svg>
<span>Only run leaders can post here</span>
</footer>
<p class="sr" aria-live="polite" data-say></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.
79 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 reactions that run a lap” from Animorio and fit it into my siteEvery reaction is a single SVG path in a 24 unit square, drawn by us so that it starts and ends at its foot, where the line comes in and goes out: the thumb starts from the line between cuff and hand, the heart from its tip, the face from a point low on the left of the circle. The details that do not fit in one line (the eyes, the mouth, the heart of the flame) are drawn after. Every drawing is sampled once with getPointAtLength every half unit; the stroke is vector-effect: non-scaling-stroke, so it is LINE pixels wide at 20 as at 28.
The route is a line through corners GAP pixels away from the message: over the bubble, down one side, under the row of reactions, then up into the pill. The measures come from offsetLeft and offsetTop summed up to the conversation, not from getBoundingClientRect, so the transforms in progress (the pressed bubble, the sliding pills) do not fool it. Every corner is rounded with its own radius (the bubble's or the pill's plus GAP): the arc starts r·tan(φ/2) from the corner, with its centre on the side the line turns to, and is sampled every two pixels; straight stretches need only their two ends. The direction is chosen so that the lap always covers three sides: from the left half of the bar clockwise, from the right half anticlockwise, passing behind the circle of the initial; with the bar under the message, or starting from a pill, the lap is complete and takes the way that never crosses itself.
The drawing in the bar (walked backwards, so the line leaves from its foot without a hairpin), the lap and the drawing in the pill become one polyline with its cumulative lengths. At every frame only a window [tail, head] of it is drawn, its ends found with a binary search: the head goes from one end to the other in LAP seconds with power2.inOut, the tail waits for 90% of LAP and then sets off with power3.in in ZIP seconds, so the line zips in like a tape measure. The line is as wide as the drawings where it draws them and TRAIL on the lap, with three 6 pixel steps on each side; the little arrow at the head turns with the tangent taken 2.5 pixels before and after, and shows only on the lap.
Before every change each pill's place is read, then the pills that move start from their old place (FLIP). A pill that goes stays where it was, out of the row in an absolute position, while it closes; a new one enters the row at once, empty and invisible, so the route knows where to end, and opens with back.out halfway through the lap. Landing, the pill swells by BOUNCE and comes back with elastic.out, the drawing's fill goes from a fill-opacity of 0.85 to 0.22 and the number rolls: the old one leaves upwards, the new one comes in from below with back.out, and the width follows with a tween.
Taking your own reaction back, the window runs backwards: the front end with power2.out, the back end with power2.in, until they meet a few hundred pixels back (230 at most) and the line fades. So the drawing unravels from its end while the head runs off, and the pill, if it was the last reaction, closes just after.
The bar is a role toolbar with a roving tabindex: arrows, Home and End move, Enter chooses, Escape closes and takes the focus back to the React button, Tab closes and goes on from there. Every button has an aria-label (React with Kudos to Maya’s message) and aria-pressed for your own reaction, and an aria-live announcement says what changed. The pointer resting 0.24 seconds on the button opens the bar; a finger kept HOLD seconds on the announcement opens it and, while that finger keeps it open, a non-passive touchmove stops the scrolling from starting, so you can slide to a reaction and lift the finger to choose it. The library's preview opens the bar, tries the reactions and picks one without taking the focus.
| Name | File | Default | What it changes |
|---|---|---|---|
REACTIONS | script.js | [['kudos', 'Kudos'], ['love', 'Love'], ['haha', 'Haha'], ['fire', 'Fire'], ['fast', 'Fast'], ['medal', 'Medal']] | The reactions of the bar: the drawing (kudos, love, haha, fire, fast or medal) and the name shown and spoken. |
LAP | script.js | 0.62 | The seconds of the lap, from the bar round the message to the reaction under it. |
ZIP | script.js | 0.34 | The seconds the rest of the line takes to zip in after the head once it has arrived. |
GAP | script.js | 7 | How many pixels from the message the line runs. |
LINE | script.js | 2 | The width of the drawings, and of the line where it draws them, in pixels. |
TRAIL | script.js | 3 | The width of the line on its lap, in pixels. |
HOLD | script.js | 0.42 | The seconds a finger stays on a message to open the bar. |
BOUNCE | script.js | 0.16 | How much a reaction swells when the line lands in it. |
DEMO | script.js | [[2, 'medal', 'bar'], [1, 'kudos', 'bar'], [2, 'fire', 'bar'], [1, 'kudos', 'chip'], [2, 'fire', 'chip']] | The library's preview, one step per click: the message (from 0), the reaction, and whether from the bar or from the pill under the message. The five steps come back to where they started. |
For reactions to the messages of a group chat, an announcements channel, the comments of a community or a sports app: wherever a reaction should feel delivered to the message, not just counted.
The chat takes the width of the screen less the margins (358 pixels at 390). A finger kept on the announcement opens the bar (the bubble sinks a little while it waits, and the phone's menu and the text selection stay shut); you choose by tapping, or by sliding the finger to the reaction and lifting it. The reactions are 30 pixels tall but their target is 44, and under 420 pixels the React button, there for the keys and the mouse, leaves its room to the bubbles and goes to the end of the row of reactions.
No line and no movement: the bar fades in and out, the chosen reaction changes colour and number with a fade in its place, a new pill fades in and one taken back fades away; the other announcements still step back while the bar is open.
GSAP 3.15.0, Archivo (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 17 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.