Skip to content
animorio
IT
All components

The clothesline menu

Navigation, Physics, Hover, SVGGSAPAdvancedCompressed weight: Demo 8.1 KBGSAP 28 KBFreeDownload the zip

The links of a menu are linen labels hung on a washing line with wooden pegs: the line is a real rope worked out every frame, and the labels hang from it like pendulums. Pointing at one, the line gives under it and its neighbours slide and swing; clicking it, it is tugged down, the line bounces and the label is dyed indigo from the bottom up.

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 clothesline menu</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bricolage-grotesque@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- a menu hung on a washing line: the line is a rope of points worked out by hand (verlet), each link is a linen
         care label held by a wooden peg, and the labels swing from the line like pendulums. Pointing at a label pulls
         it down, a click tugs it and dyes it indigo -->
    <nav class="line" aria-label="Main" data-line>
      <svg class="line__rope" aria-hidden="true" data-rope></svg>
      <ul class="line__tags">
        <li><a class="tag" href="#shop" aria-current="page" data-tag>
          <span class="tag__cloth">
            <span class="tag__face">
              <svg class="tag__care" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 8l2.2 10.5h13.6L21 8"/><path d="M3.6 10.5c1.5-1.2 3-1.2 4.4 0s2.9 1.2 4 0 2.9-1.2 4 0 2.9 1.2 4.4 0"/></svg>
              <span class="tag__name">Shop</span>
            </span>
            <span class="tag__dye" aria-hidden="true" data-dye><span class="tag__face">
              <svg class="tag__care" viewBox="0 0 24 24"><path d="M3 8l2.2 10.5h13.6L21 8"/><path d="M3.6 10.5c1.5-1.2 3-1.2 4.4 0s2.9 1.2 4 0 2.9-1.2 4 0 2.9 1.2 4.4 0"/></svg>
              <span class="tag__name">Shop</span>
            </span></span>
          </span>
          <svg class="tag__peg" viewBox="0 0 18 44" aria-hidden="true"><rect class="peg__shade" x="1.6" y="3.6" width="16" height="41.4" rx="3"/><path class="peg__wood" d="M2.4 1h5.2a1.4 1.4 0 0 1 1.4 1.4V40a3 3 0 0 1-3 3h-1.4a3 3 0 0 1-3-3V2.4A1.4 1.4 0 0 1 2.4 1z"/><path class="peg__wood peg__wood--back" d="M10.4 1h5.2A1.4 1.4 0 0 1 17 2.4V40a3 3 0 0 1-3 3h-1.4a3 3 0 0 1-3-3V2.4A1.4 1.4 0 0 1 10.4 1z"/><path class="peg__grain" d="M4.3 5v8M13.7 6v6M4.6 26v11M13.4 27v10"/><rect class="peg__spring" x="0.4" y="16" width="17.2" height="5.4" rx="2.7"/><path class="peg__coil" d="M4 16.4v4.6M7 16.4v4.6M10 16.4v4.6M13 16.4v4.6"/></svg>
        </a></li>
        <li><a class="tag" href="#bedding" data-tag data-autoclic>
          <span class="tag__cloth">
            <span class="tag__face">
              <svg class="tag__care" viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="3.5" width="17" height="17" rx="1"/><circle cx="12" cy="12" r="5.6"/></svg>
              <span class="tag__name">Bedding</span>
            </span>
            <span class="tag__dye" aria-hidden="true" data-dye><span class="tag__face">
              <svg class="tag__care" viewBox="0 0 24 24"><rect x="3.5" y="3.5" width="17" height="17" rx="1"/><circle cx="12" cy="12" r="5.6"/></svg>
              <span class="tag__name">Bedding</span>
            </span></span>
          </span>
          <svg class="tag__peg" viewBox="0 0 18 44" aria-hidden="true"><rect class="peg__shade" x="1.6" y="3.6" width="16" height="41.4" rx="3"/><path class="peg__wood" d="M2.4 1h5.2a1.4 1.4 0 0 1 1.4 1.4V40a3 3 0 0 1-3 3h-1.4a3 3 0 0 1-3-3V2.4A1.4 1.4 0 0 1 2.4 1z"/><path class="peg__wood peg__wood--back" d="M10.4 1h5.2A1.4 1.4 0 0 1 17 2.4V40a3 3 0 0 1-3 3h-1.4a3 3 0 0 1-3-3V2.4A1.4 1.4 0 0 1 10.4 1z"/><path class="peg__grain" d="M4.3 5v8M13.7 6v6M4.6 26v11M13.4 27v10"/><rect class="peg__spring" x="0.4" y="16" width="17.2" height="5.4" rx="2.7"/><path class="peg__coil" d="M4 16.4v4.6M7 16.4v4.6M10 16.4v4.6M13 16.4v4.6"/></svg>
        </a></li>
        <li><a class="tag" href="#kitchen" data-tag>
          <span class="tag__cloth">
            <span class="tag__face">
              <svg class="tag__care" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 17.5h18c0-4.6-2.6-8.5-8-8.5H8.5"/><path d="M8.5 9V6.5h7"/><circle cx="12" cy="14" r="0.9"/></svg>
              <span class="tag__name">Kitchen</span>
            </span>
            <span class="tag__dye" aria-hidden="true" data-dye><span class="tag__face">
              <svg class="tag__care" viewBox="0 0 24 24"><path d="M3 17.5h18c0-4.6-2.6-8.5-8-8.5H8.5"/><path d="M8.5 9V6.5h7"/><circle cx="12" cy="14" r="0.9"/></svg>
              <span class="tag__name">Kitchen</span>
            </span></span>
          </span>
          <svg class="tag__peg" viewBox="0 0 18 44" aria-hidden="true"><rect class="peg__shade" x="1.6" y="3.6" width="16" height="41.4" rx="3"/><path class="peg__wood" d="M2.4 1h5.2a1.4 1.4 0 0 1 1.4 1.4V40a3 3 0 0 1-3 3h-1.4a3 3 0 0 1-3-3V2.4A1.4 1.4 0 0 1 2.4 1z"/><path class="peg__wood peg__wood--back" d="M10.4 1h5.2A1.4 1.4 0 0 1 17 2.4V40a3 3 0 0 1-3 3h-1.4a3 3 0 0 1-3-3V2.4A1.4 1.4 0 0 1 10.4 1z"/><path class="peg__grain" d="M4.3 5v8M13.7 6v6M4.6 26v11M13.4 27v10"/><rect class="peg__spring" x="0.4" y="16" width="17.2" height="5.4" rx="2.7"/><path class="peg__coil" d="M4 16.4v4.6M7 16.4v4.6M10 16.4v4.6M13 16.4v4.6"/></svg>
        </a></li>
        <li><a class="tag" href="#journal" data-tag>
          <span class="tag__cloth">
            <span class="tag__face">
              <svg class="tag__care" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3.8l9 15.7H3z"/></svg>
              <span class="tag__name">Journal</span>
            </span>
            <span class="tag__dye" aria-hidden="true" data-dye><span class="tag__face">
              <svg class="tag__care" viewBox="0 0 24 24"><path d="M12 3.8l9 15.7H3z"/></svg>
              <span class="tag__name">Journal</span>
            </span></span>
          </span>
          <svg class="tag__peg" viewBox="0 0 18 44" aria-hidden="true"><rect class="peg__shade" x="1.6" y="3.6" width="16" height="41.4" rx="3"/><path class="peg__wood" d="M2.4 1h5.2a1.4 1.4 0 0 1 1.4 1.4V40a3 3 0 0 1-3 3h-1.4a3 3 0 0 1-3-3V2.4A1.4 1.4 0 0 1 2.4 1z"/><path class="peg__wood peg__wood--back" d="M10.4 1h5.2A1.4 1.4 0 0 1 17 2.4V40a3 3 0 0 1-3 3h-1.4a3 3 0 0 1-3-3V2.4A1.4 1.4 0 0 1 10.4 1z"/><path class="peg__grain" d="M4.3 5v8M13.7 6v6M4.6 26v11M13.4 27v10"/><rect class="peg__spring" x="0.4" y="16" width="17.2" height="5.4" rx="2.7"/><path class="peg__coil" d="M4 16.4v4.6M7 16.4v4.6M10 16.4v4.6M13 16.4v4.6"/></svg>
        </a></li>
        <li><a class="tag" href="#visit" data-tag>
          <span class="tag__cloth">
            <span class="tag__face">
              <svg class="tag__care" viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="3.5" width="17" height="17" rx="1"/><path d="M6 5.5c3.6 3 8.4 3 12 0"/></svg>
              <span class="tag__name">Visit</span>
            </span>
            <span class="tag__dye" aria-hidden="true" data-dye><span class="tag__face">
              <svg class="tag__care" viewBox="0 0 24 24"><rect x="3.5" y="3.5" width="17" height="17" rx="1"/><path d="M6 5.5c3.6 3 8.4 3 12 0"/></svg>
              <span class="tag__name">Visit</span>
            </span></span>
          </span>
          <svg class="tag__peg" viewBox="0 0 18 44" aria-hidden="true"><rect class="peg__shade" x="1.6" y="3.6" width="16" height="41.4" rx="3"/><path class="peg__wood" d="M2.4 1h5.2a1.4 1.4 0 0 1 1.4 1.4V40a3 3 0 0 1-3 3h-1.4a3 3 0 0 1-3-3V2.4A1.4 1.4 0 0 1 2.4 1z"/><path class="peg__wood peg__wood--back" d="M10.4 1h5.2A1.4 1.4 0 0 1 17 2.4V40a3 3 0 0 1-3 3h-1.4a3 3 0 0 1-3-3V2.4A1.4 1.4 0 0 1 10.4 1z"/><path class="peg__grain" d="M4.3 5v8M13.7 6v6M4.6 26v11M13.4 27v10"/><rect class="peg__spring" x="0.4" y="16" width="17.2" height="5.4" rx="2.7"/><path class="peg__coil" d="M4 16.4v4.6M7 16.4v4.6M10 16.4v4.6M13 16.4v4.6"/></svg>
        </a></li>
      </ul>
    </nav>
  </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.

29 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 clothesline menu” from Animorio and fit it into my site

What it does

On a white background, alone in the middle, a washing line of twisted cord hangs between two brass rings; pegged to it with wooden pegs are five linen labels with a stitched edge, a laundry care symbol and a name: Shop, Bedding, Kitchen, Journal, Visit. The one of the current page is dyed indigo. As the page loads the labels drop onto the line one after the other and swing; then a light breeze keeps the line always moving a little. With the pointer over a label, it drops a few pixels pulling the line, and its neighbours slide towards it and sway. Clicking it, the label is tugged down sharply and let go: the line bounces up like elastic, all the labels swing, the indigo rises from the bottom with a wavy edge like cloth soaking up the colour and the one of the previous label drains away.

How it works

A rope of points

The line is a chain of 41 points between two fixed rings, eight for each label. Every frame it takes 16 small steps: each point moves on with its own speed (Verlet, held back by the air) and gravity, then each link brings its two points back to its rest length. With many small steps and one round of constraints per step the rope stays almost inextensible and never jitters, even at 390 pixels.

A line that gives (XPBD)

The constraints are XPBD ones: each has a compliance, GIVE · 1e-9 · the length of the line, divided by the step squared, which lets the line stretch under a weight. Scaled with the length, the line hangs and gives the same way on a computer and on a phone. The labels' points weigh WEIGHT times the others and the air holds them back more: they are cloth.

Pulling a point

Pointing at a label adds a soft constraint that takes its point DIP pixels below its rest height, measured once the line has settled. The click moves that height TUG pixels with a GSAP tween (sine.inOut, 0.16 seconds) and then removes the constraint: the taut line springs back on its own, and the bounce and the neighbours' swing are not animated, they come from the physics.

The labels are pendulums

Each label turns round its peg like a pendulum: the sideways acceleration of its point pushes it, a spring brings it back down at frequency SWAY[0] and SWAY[1] settles it. A breeze made of two slow waves pushes the line sideways, so nothing is ever quite still. The line is drawn in SVG as a smooth curve (Catmull-Rom into Béziers) with its shadow, the dashed twist of the cord and a thread of light.

The dye and the keys

Each label has over it a copy of itself dyed indigo, cut by a polygon clip-path whose edge waves and rises in DYE seconds. The links are real links in a nav, with aria-current="page"; the arrows, Home and End run along the line, and keyboard focus pulls the label like the pointer.

Values to change

NameFileDefaultWhat it changes
SAGscript.js0.05How much the line sags, as a share of its length.
GIVEscript.js2How much the line stretches under a pull: higher, a more elastic line.
DIPscript.js14How many pixels a label drops under the pointer.
TUGscript.js46How many pixels the click tugs the label down (less on a phone, in proportion to the line).
SWAYscript.js[5.5, 2.6]The labels' swing: how quick it is (the pendulum's length) and how soon it settles.
BREEZEscript.js0.35A light breeze that keeps the line moving, from 0 (still) to 1.
DYEscript.js0.75The seconds the indigo takes to rise up the chosen label.

Try them in the Playground

When to use it

For the navigation of a site with a handmade character (textiles, clothing, a laundry, a holiday home, a workshop) with four or five short links, at the top of the page where there is room for the line.

On phones

The line takes the whole width and the labels narrow to 58 pixels, so five fit in 390 pixels; the line sags and gives in proportion to its length and the click's tug gets shorter. At a touch the pressing finger pulls the label down like the pointer, and lifting it chooses it.

With reduced motion

The line stays still in its resting curve, with no breeze and no swing, and the labels are already hanging when the page loads; choosing one, the indigo fades in.

Pitfalls

  • With few steps and many rounds of constraints (classic Gauss-Seidel) the line stretched by 70 per cent and sagged like slack elastic: with 16 small steps and a single round per step it stays almost inextensible.
  • An inextensible line cannot be pulled down: under the pointer the label dropped two pixels. XPBD compliance gives it the right stretch, and it must be multiplied by the length of the line, or on a phone the line is stiff and on a computer it is slack.
  • The point to pull to must be measured after the line has settled: worked out from the theoretical parabola it was higher than the real line, and under the pointer the label rose instead of dropping.
  • The click used to pull to a point in one go: the line bounced back twice as high and the labels jumped as on a trampoline. The tug now goes down with a tween, and the air holds the labels back more than the cord.
  • The line redrawn every frame made whatever lay under it repaint too, and three quarters of the time went there: the line sits on a layer of its own (will-change), and its shadow is a wide pale stroke instead of a blur.
  • The time step is capped at 1/30 of a second and split into 16: after a tab in the background a jump in time tore the line.

Libraries

GSAP 3.15.0, Bricolage Grotesque (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 8.1 KB; the libraries: GSAP 28 KB.

Testing

Tested on 4 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: Done

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