Skip to content
animorio
IT
Back to the library

Projects with elastic lines

Hover, Physics, SVGCSS onlyMediumCompressed weight: Demo 5.6 KBno librariesFreeDownload the zip

A list of products with one big name per row. Over a name the word opens in two and a picture of the product appears in the gap; the lines between the names catch on the pointer, bend with it and, when it pulls too far, slip away and spring back straight, quivering.

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>Projects with elastic lines</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/dynapuff@5.3.0/index.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/dm-mono@5.3.0/500.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <!-- the list of a small studio's products: over a name, the word parts in two and a little picture of the product
       slides into the gap; the lines between the names can be caught with the pointer, bent and let go -->
  <main class="pagina">
    <header class="testa">
      <span>Otter &amp; Oak</span>
      <span>Five things we built</span>
    </header>

    <ul class="elenco" data-elenco>
      <li class="voce">
        <a class="riga" href="#" data-riga aria-label="Tidepool, tide tables for surfers">
          <span class="anteprima" aria-hidden="true">
            <svg viewBox="0 0 300 150"><rect width="300" height="150" rx="22" fill="#2f7dd9"/><path d="M0 92C40 78 70 104 110 92S180 78 220 92S280 104 300 92V150H0Z" fill="#7fc4ff"/><circle cx="78" cy="58" r="18" fill="#ffe08a"/><path d="M212 40l7 15 16 2-12 11 3 16-14-8-14 8 3-16-12-11 16-2z" fill="#ff8a5c"/><text x="22" y="134" fill="#0d2a4d">tide tables for surfers</text></svg>
          </span>
          <span class="parola" aria-hidden="true"><span class="meta meta--sx" data-sx>TIDE</span><span class="meta meta--dx" data-dx>POOL</span></span>
        </a>
      </li>
      <li class="voce">
        <svg class="filo" data-filo aria-hidden="true" focusable="false"><path/></svg>
        <a class="riga" href="#" data-riga aria-label="Lantern, a night bus planner">
          <span class="anteprima" aria-hidden="true">
            <svg viewBox="0 0 300 150"><rect width="300" height="150" rx="22" fill="#1d1a3a"/><rect x="120" y="34" width="60" height="78" rx="16" fill="#ffb347"/><rect x="134" y="48" width="32" height="50" rx="9" fill="#ffe7b3"/><path d="M132 34c0-16 36-16 36 0" fill="none" stroke="#ffb347" stroke-width="7"/><text x="22" y="134" fill="#c9c3ff">a night bus planner</text></svg>
          </span>
          <span class="parola" aria-hidden="true"><span class="meta meta--sx" data-sx>LAN</span><span class="meta meta--dx" data-dx>TERN</span></span>
        </a>
      </li>
      <li class="voce">
        <svg class="filo" data-filo aria-hidden="true" focusable="false"><path/></svg>
        <a class="riga" href="#" data-riga aria-label="Pebble, a savings jar for kids">
          <span class="anteprima" aria-hidden="true">
            <svg viewBox="0 0 300 150"><rect width="300" height="150" rx="22" fill="#f2e2c9"/><ellipse cx="150" cy="98" rx="62" ry="22" fill="#9b8a74"/><ellipse cx="146" cy="68" rx="44" ry="17" fill="#c4b39b"/><ellipse cx="152" cy="44" rx="27" ry="12" fill="#e0d2bb"/><text x="22" y="134" fill="#5b4a35">a savings jar for kids</text></svg>
          </span>
          <span class="parola" aria-hidden="true"><span class="meta meta--sx" data-sx>PEB</span><span class="meta meta--dx" data-dx>BLE</span></span>
        </a>
      </li>
      <li class="voce">
        <svg class="filo" data-filo aria-hidden="true" focusable="false"><path/></svg>
        <a class="riga" href="#" data-riga aria-label="Hayloft, bookings for farm stays">
          <span class="anteprima" aria-hidden="true">
            <svg viewBox="0 0 300 150"><rect width="300" height="150" rx="22" fill="#ffd54d"/><path d="M104 112V62l46-30 46 30v50z" fill="#d9452b"/><path d="M134 112V80h32v32" fill="#ffd54d"/><path d="M134 80l32 32M166 80l-32 32" stroke="#d9452b" stroke-width="5"/><text x="22" y="134" fill="#6b4300">bookings for farm stays</text></svg>
          </span>
          <span class="parola" aria-hidden="true"><span class="meta meta--sx" data-sx>HAY</span><span class="meta meta--dx" data-dx>LOFT</span></span>
        </a>
      </li>
      <li class="voce">
        <svg class="filo" data-filo aria-hidden="true" focusable="false"><path/></svg>
        <a class="riga" href="#" data-riga aria-label="Mosswork, a shared garden diary">
          <span class="anteprima" aria-hidden="true">
            <svg viewBox="0 0 300 150"><rect width="300" height="150" rx="22" fill="#3f8f4f"/><path d="M70 104c-8-30 18-50 40-36 6-24 40-28 50-6 20-14 46 2 40 28 18 4 20 22 6 30H76c-14-2-18-12-6-16z" fill="#a8e06b"/><circle cx="118" cy="88" r="7" fill="#f6ffe0"/><circle cx="176" cy="80" r="5" fill="#f6ffe0"/><text x="22" y="134" fill="#e6ffd2">a shared garden diary</text></svg>
          </span>
          <span class="parola" aria-hidden="true"><span class="meta meta--sx" data-sx>MOSS</span><span class="meta meta--dx" data-dx>WORK</span></span>
        </a>
      </li>
    </ul>
  </main>

  <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 “Projects with elastic lines” from Animorio and fit it into my site

What it does

The product page of a made-up studio of two people, Otter & Oak, on a pale sage green: five huge names in a round, puffy typeface, one per row, divided by thin lines. When the page opens the lines draw themselves from left to right, one after another. With the pointer on a row, the name splits at its own point, TIDE and POOL, LAN and TERN, and the two halves slide apart in half a second, just enough for a picture of the product to fit in between, with a line saying what it does; the picture appears growing a touch. If the pointer passes less than twenty pixels from a line, the line catches it: its middle follows, left and right and a little up or down, and the line curves. If the pointer moves more than twenty-five pixels away or leaves the row, the line lets go and springs back straight, swinging a few times, with the speed it had when it slipped away.

How it works

The two halves

Every name is two spans side by side. The script measures where the first half ends and where the second begins and writes into two CSS variables how far each must move so that the picture, one and a half times as wide as the letters are tall, sits in the middle with ROOM to spare on each side. With the row open, a SPLIT-second transition moves the halves on the curve 0, 0, 0.2, 1. The measurements come from the layout, which transforms do not change: they can be taken again with a row open.

The open row

Which row opens is decided by the script, not by :hover: on every mouse move it looks at the element under the pointer and opens its row. That way it also works with the preview’s autopilot, with the keyboard (a row opens when it gets the focus) and with a finger, which opens the row it touches and closes it on the second touch.

The lines

Every line is an SVG as tall as a row and centred on its edge, with a curve from the left side to the right and a single control point, in the middle while it is straight. The pointer catches it when it passes less than GRAB pixels away; from then on the control point sits under the pointer, pushed PULL times further from the middle (a curve only gets half way to its control point, so the line passes close to the pointer). The line slips away if the pointer moves more than LET_GO pixels off, or half a row if that is less, or leaves the box.

The spring

Once free, the control point goes back to the middle on a spring: on every frame, in steps of a two hundred and fortieth of a second, the force is the distance times the stiffness minus the speed times the damping, the two numbers of SPRING. The starting speed is the hand’s in the last frame: a line yanked away quickly swings more.

The opening draw

At the start every line has pathLength 1, so it draws itself with stroke-dashoffset alone, from 1 to 0, pushed by another spring, the two numbers of DRAW. The first starts after DRAW_DELAY[0] seconds and the others one after the other, DRAW_DELAY[1] seconds later.

Values to change

NameFileDefaultWhat it changes
SPLITscript.js0.5The seconds the two halves take to part (as on the site).
ROOMscript.js0.18The room on each side of the picture, as a share of the letter size.
GRABscript.js20How close to the line the pointer must pass to catch it, in pixels (20 on the site).
LET_GOscript.js25How far the line can be pulled before it slips away, in pixels (25 on the site).
PULLscript.js2.2How many times further than the pointer the curve’s control point moves (2.2 on the site).
SPRINGscript.js[200, 10]The spring back: stiffness and damping. On the site they are 200 and 30, and the line comes back without swinging.
DRAWscript.js[300, 30]The spring of the lines’ opening draw (the site’s).
DRAW_DELAYscript.js[0.15, 0.08]The wait before the first line and between one line and the next, in seconds.

Try them in the Playground

When to use it

For a studio’s list of work or a brand’s products, when the names are few and can be big: the picture only shows to whoever passes over it, and the lines give something to touch even to those who are just looking.

On phones

The same: tap a name to open it and tap again to close it, and drag a finger across a line to catch it and bend it just as with the mouse.

With reduced motion

The halves open at once, without sliding, the picture fades in and the lines stay straight.

Pitfalls

  • The halves measure their position from the nearest box that has a position, which is the row: adding the word’s position on top counts it twice, and one half runs off the screen while the other stays put.
  • :hover does not respond to the events made up by the preview’s autopilot: the open row is chosen by the script, looking at what is under the pointer.
  • With a finger the page would scroll instead of bending the line: the list has touch-action: none, and the page fits on one screen.

Libraries

DynaPuff (font variabile) 5.3.0, DM Mono 5.3.0.

Compressed with gzip, the three demo files weigh 5.6 KB, and no libraries are needed.

Testing

Tested on 6 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