Skip to content
animorio
IT
All components

The hanging mobile menu

Menus, Physics, Hover, TransitionsCanvas, GSAPAdvancedCompressed weight: Demo 14 KBGSAP 28 KBFreeDownload the zip

A small gallery's menu as a hanging mobile in the manner of Calder: at rest it is a button with a thread hanging from it and a little copy of the shape of the page you are on. Pressed, the thread drops and the wire arms open one under the other, each hung from the arm above; every entry is a flat painted shape (petal, disc, crescent) hanging at one end. The balance is real: the mobile swings and settles where the moments of the weights even out. Hovered, a shape turns on its thread and its arm tips under the weight; you can take one and swing the whole thing.

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 hanging mobile menu</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bricolage-grotesque@5.3.0/standard.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the hanging mobile menu: the menu of a small gallery as a hanging mobile. Closed, it is a button with a thread
         hanging from it and, at its end, a little copy of the shape of the page you are on. Pressed, the thread drops
         and the wire arms open one under the other, each balanced under its own thread; every entry is a flat painted
         shape hanging at the end of an arm. The arms really balance: every shape has its weight, and the mobile swings
         and settles where the moments of the forces even out -->
    <div class="mobile" data-mobile>
      <canvas class="mobile__canvas" aria-hidden="true" data-canvas></canvas>

      <nav class="mobile__nav" id="mobile-menu" aria-label="Main" data-nav inert>
        <ul class="mobile__list">
          <li><a class="piece" href="#exhibitions" style="--c: var(--red)" data-piece="petal" aria-current="page"><span class="piece__label">Exhibitions</span></a></li>
          <li><a class="piece" href="#artists" style="--c: var(--black)" data-piece="disc"><span class="piece__label">Artists</span></a></li>
          <li><a class="piece" href="#collection" style="--c: var(--yellow)" data-piece="moon"><span class="piece__label">Collection</span></a></li>
          <li><a class="piece" href="#visit" style="--c: var(--blue)" data-piece="leaf"><span class="piece__label">Visit</span></a></li>
          <li><a class="piece" href="#journal" style="--c: var(--white)" data-piece="dot"><span class="piece__label">Journal</span></a></li>
        </ul>
      </nav>

      <button class="hanger" type="button" aria-expanded="false" aria-controls="mobile-menu" data-hanger data-autoclic>
        <span class="hanger__bg" aria-hidden="true" data-bg></span>
        <span class="hanger__icon" aria-hidden="true">
          <svg class="hanger__glyph" viewBox="0 0 24 24" data-glyph>
            <path class="glyph__wire" d="M12 2.5V7M4.5 10.5Q12 5.5 19.5 10.5M4.5 10.5V13M19.5 10.5V12.5"/>
            <path class="glyph__shape glyph__shape--red" d="M4.5 13c2.6.8 3.2 4.4.4 6.6-2.4-.9-3.1-4.4-.4-6.6z"/>
            <path class="glyph__shape" d="M19.5 12.5a3.6 3.6 0 1 1 0 7.2 3.6 3.6 0 0 1 0-7.2z"/>
          </svg>
          <svg class="hanger__cross" viewBox="0 0 24 24" data-cross>
            <path d="M6.5 6.5l11 11M17.5 6.5l-11 11"/>
          </svg>
        </span>
        <span class="hanger__words" aria-hidden="true">
          <span class="hanger__word" data-word="menu">Menu</span>
          <span class="hanger__word" data-word="close">Close</span>
        </span>
        <span class="sr" data-name>Open the menu</span>
      </button>
      <p class="sr" aria-live="polite" data-live></p>
    </div>
  </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 hanging mobile menu” from Animorio and fit it into my site

What it does

On a white background a black pill button with Menu and a little drawn mobile; below it, from a brass eyelet, hangs a thin thread with a small copy of the red petal of Exhibitions, the page you are on, and its soft shadow on the wall. Hovering, the word widens, the pill swells with a bounce and the petal sways. Pressed, the thread drops and the steel arms open one under the other, each hung by a thread from the end of the arm above: at every free end a painted shape grows with its caption, Exhibitions (red petal), Artists (black disc), Collection (yellow crescent), Visit (blue petal), Journal (white disc). The arms open, swing and come to rest level, because each one hangs exactly where the weights on its two sides even out; the button says Close. The shapes are thin sheets: turning on the thread they narrow, show their edge, and the light slides over the paint. Hovering a shape, its caption widens and darkens, the shape turns slowly on its thread and weighs a little more: its arm tips and the ones above answer. A shape can be taken with the pointer and pulled: the whole mobile swings and, let go, carries on by itself. Choosing an entry, its shape drops a little on its thread and makes one turn in the light, the others gather in, the arms fold up from the bottom and the thread rises back into the button carrying the small copy of the chosen shape. From the keyboard the arrows go through the entries (the shape with the focus turns and weighs), Enter chooses, Escape closes; a click outside closes.

How it works

Points, weights and threads

The mobile is a set of points with a weight: the pivot and the two ends of every arm (light wire) and one shape per entry (heavy, roughly as much as its area). The points move with Verlet steps of 1/120 of a second, with gravity and an exponential damping; the threads are constraints that only pull (a maximum distance), solved ten times per step with every correction weighted by the inverse of the mass (position based dynamics). The fixed step keeps the physics the same at any screen rate.

Every arm hung at the right point

From the bottom up the script adds up the weight hanging under each end and puts each arm's pivot where the weight on the left times its arm equals the weight on the right times its own: so the mobile, let go, comes to rest level by itself. The pivot sits a little above the line of the ends (CURVE), like the curved arms of real mobiles: that is what makes the balance stable, and lets an extra weight tip the arm by a fair angle instead of turning it over.

Rigid arms with shape matching

After every pass over the threads, the three points of an arm are put back on their shape: the script finds the rotation that fits them best (in two dimensions a single atan2 over the sums of dot and cross products) and applies it round their centre. A rotation never mirrors, so an arm cannot flip over even while it opens from a single point. Long tethers from the eyelet to every point, as long as the thread and wire between them, keep the chain of heavy pieces from stretching at every joint.

Opening, closing, choosing

A GSAP timeline pays out each arm's thread (sine.inOut, never faster than free fall, so it does not jerk) and spreads the arm from its pivot, one after another STAGGER seconds apart; the shapes grow with elastic.out. The timeline moves on the physics' own clock, in steps of 1/120: on a slow computer they slow down together. Choosing, the shape's thread grows by DROP and the others shrink to nothing; then the arms fold up from the bottom and the first thread turns short again, with the small copy of the chosen shape.

Sheets turning in the light

Each shape is a Path2D drawn on the canvas from the point it hangs by, turned like its thread. It also turns round the thread: its width is multiplied by the cosine of the angle, a dark copy shifted by the sine shows under it (the edge of the sheet), the colour brightens as the face looks at the lamp and a band of light slides from one side to the other. Under the pointer the shape turns at SPIN turns a second and its weight becomes HEAVY times as much: the arm tips under the new moments. The shadow on the wall is the same drawing on a canvas a quarter the size, scaled up: it softens by itself, without the blur filter.

Real links and keys

The entries are links in a nav (inert while closed) and follow the shapes every frame, with the caption under them; leaving the hover is judged from the shape, not the box, so a shape sinking under the mouse does not lose it. Dragging one, its point follows the hand with a soft limit (tanh); a tap without a drag chooses. The button has aria-expanded and aria-controls, the arrows, Home and End move the focus, Escape closes and brings the focus back to the button, an aria-live area reads the chosen entry. When nothing moves the loop stops.

Values to change

NameFileDefaultWhat it changes
GRAVITYscript.js2400How hard the shapes are pulled down, in units of the drawing per second squared: stronger, the mobile swings faster.
DAMPINGscript.js2.2How quickly the swinging dies away, per second (0 = it never stops).
HEAVYscript.js1.14How many times heavier a shape gets under the pointer: its arm tips under the weight.
SPINscript.js0.32How many turns a second a shape makes on its thread under the pointer.
CURVEscript.js0.11How high each arm arches over its pivot, as a share of its length: higher, it tips less.
STAGGERscript.js0.2The seconds between one arm opening and the next.
DROPscript.js30How far the chosen shape drops on its thread before the mobile folds away.

Try them in the Playground

When to use it

For the menu of a site where shape and colour matter: a gallery, a museum, a design or architecture studio, a shop of objects, a portfolio. From four to six short entries; each one is known by its shape, and the small copy hanging from the button says which page you are on even while closed.

On phones

The whole mobile scales to the width of the screen (at 390 pixels the frame is 358 wide) and fits when open, with the captions never under 11.5 pixels. A finger holding a shape counts as the mouse hovering it (it turns and weighs), dragging it swings the whole thing, a tap chooses it; the shapes keep touch-action: none so the finger does not scroll the page.

With reduced motion

No swinging or turning: the mobile opens and closes with a fade, already balanced in its place, and on hover only the caption changes. The chosen entry closes the menu with the same fade and its small copy already hangs from the button.

Pitfalls

  • With three distance constraints per arm, while an arm opened from a single point the first one flipped over like a mirror image and stayed upside down: now every arm is put back on its shape by a rotation (shape matching), which cannot mirror.
  • With the pivot on the line of the ends the balance was neutral and the arms stayed crooked wherever they happened to stop: the pivot sits above the line, as in real curved arms.
  • In a slow browser the physics, with its step capped at 1/30, fell behind the timelines and the arms were thrown open at once, flailing: the timelines now move on the physics' clock.
  • The thread paying out faster than free fall let the shapes drop and then stopped them dead, so everything bounced: the thread pays out with sine.inOut and goes along with the fall.
  • The shadow softened with the canvas blur filter brought the test computer down to a few frames a second, and Safari does not have it: the shadow is drawn on a small canvas and scaled up.
  • A shape tipping and sinking under the mouse slipped out of its box, lost the hover, rose back and took it again, forever: the hover now ends only when the pointer moves away from the shape.
  • Dragging a shape far pulled half the mobile out of its frame: the hand pulls with a soft limit.

Libraries

GSAP 3.15.0, Bricolage Grotesque (font variabile) 5.3.0.

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

Testing

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