Skip to content
animorio
IT
All components

The reactions that run a lap

Chat, SVG, Transitions, HoverGSAPAdvancedCompressed weight: Demo 17 KBGSAP 28 KBFreeDownload the zip

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.

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

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.

79 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 reactions that run a lap” from Animorio and fit it into my site

What it does

On a black background a dark chat 390 pixels wide: at the top the circle with the drawn track, Lockside Run Club and Announcements · 46 members; below, four announcements by Maya, Jonah and Priya, each with the circle of the initial, the name inside the bubble and the time at the bottom right, and under each announcement its reactions: small pills with a one-line drawing in its colour (Kudos blue, Love pink, Haha yellow, Fire orange, Fast teal, Medal violet) and the number. My reaction has its pill tinted and edged in its colour. At the bottom, the lock and Only run leaders can post here. Passing the pointer over an announcement shows the round button to react beside it; resting the pointer on it, clicking it, or keeping a finger pressed on the announcement opens a dark bar over it with the six reactions coming in one after the other with a bounce, while the other announcements step back. The reaction under the pointer, the finger or the keys lifts and grows with its name over it. Once one is chosen, the bar goes and its drawing stays there and unravels: the line, with a little arrow at its head, comes down to the announcement and runs round it a few pixels from the edge, over it, down one side (passing behind the circle of the initial too) and under the reactions, then goes up into the pill and draws the reaction again stroke by stroke. For a moment the whole route, from the drawing in the bar to the one in the pill, hugs the message; then the tail sets off and zips in faster and faster, the drawing in the bar unravels and the whole line ends inside the reaction, which swells and settles with a bounce, warms up and cools down in its colour, while the number rolls up and the pill takes its tint. A new reaction opens its empty place as the line comes near. Tapping someone else's reaction, the line leaves from the pill itself, runs the whole lap round the message and comes back there; tapping your own, the drawing unravels out of the pill, runs off backwards along the edge and fades, the number goes down and, if it was the last, the pill closes and the others slide into its place. Changing reaction, the old one leaves and the new one arrives at the same time. For the first announcement, with no room over it, the bar opens under it and the lap is complete on four sides.

How it works

One-line drawings

Every 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 lap round the message

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.

A window on a single line

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.

The place and the landing

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.

Leaving backwards

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.

Bar, keys and finger

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.

Values to change

NameFileDefaultWhat it changes
REACTIONSscript.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.
LAPscript.js0.62The seconds of the lap, from the bar round the message to the reaction under it.
ZIPscript.js0.34The seconds the rest of the line takes to zip in after the head once it has arrived.
GAPscript.js7How many pixels from the message the line runs.
LINEscript.js2The width of the drawings, and of the line where it draws them, in pixels.
TRAILscript.js3The width of the line on its lap, in pixels.
HOLDscript.js0.42The seconds a finger stays on a message to open the bar.
BOUNCEscript.js0.16How much a reaction swells when the line lands in it.
DEMOscript.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.

Try them in the Playground

When to use it

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.

On phones

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.

With reduced motion

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.

Pitfalls

  • With the lap always clockwise, picking a reaction on the right of the bar (over a short message) the line only drew an L, down and under: the direction is chosen from the half of the bar, so the lap always covers three sides.
  • Over the first message the bar ended up on the head and covered the chat's name, and the start of the line was hidden under it: when there is no room over the message the bar opens under it, and the lap becomes complete, the way that never crosses itself.
  • Measuring with getBoundingClientRect, the bubble still pressed or the pills halfway through their slide gave wrong places and the line came in askew: the measures come from offsetLeft and offsetTop, which ignore transforms.
  • Leaving from the end of the drawing in the bar the line made a hairpin turn to go down: the drawing in the bar is walked backwards, so the line leaves from its foot, as it comes into the pill.
  • At 2 pixels the line could hardly be seen in the catalogue's thumbnail: on the lap it is TRAIL wide, and to avoid a step where it meets the drawings it goes from one width to the other in three steps.
  • On a phone the React button, not used there, took 52 pixels from the bubbles: they grew to three lines and the first message ended up under the head. Under 420 pixels the button leaves the flow.
  • A finger sliding towards the bar after the long press starts the scrolling and the browser cancels the pointer: while the bar that finger opened is open, a non-passive touchmove prevents it.

Libraries

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.

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