Skip to content
animorio
IT
All components

The signpost menu

Menus, 3D, Physics, HoverGSAP, CSS onlyAdvancedCompressed weight: Demo 9.7 KBGSAP 28 KBFreeDownload the zip

The menu of a festival in the woods as a post with painted wooden arrows: closed, it is the oak board at the top of the post, with Menu burnt into the wood. Pressed, the board spins on the post and its back reads Close, the post slides down, bounces and sets the sign swaying, and each arrow, as the post reaches it, is flung out to the left or the right and wobbles on its spring, passing the wobble to the others. The pointer brushing an arrow pushes it back and sets it swinging, with its letters bouncing; the chosen one flips over on its long side.

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 signpost menu</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/shantell-sans@5.3.0/full.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the menu of a summer festival in the woods as a wooden signpost. Closed, the button is the oak board nailed to
         the top of the post. Pressed, the board spins round on the post and reads Close, the post slides down out of
         it, and as its end goes past each arrow the arrow is flung out round the post, left and right in turn, and
         wobbles on its spring; the wobble runs up and down the post into the other arrows. The pointer brushing an
         arrow pushes it back and lets it swing; a chosen arrow flips over on its long side. The boards are flat
         elements turned in 3D by the script, with their thickness made of thin slices -->
    <nav class="sign" aria-label="Festival" data-sign>
      <div class="sign__frame" data-frame>
        <div class="sign__scene" data-scene>
          <span class="pole" aria-hidden="true" data-pole><span class="pole__wood"></span></span>
          <svg class="cap" viewBox="0 0 34 14" aria-hidden="true"><path d="M3 13 L17 1 L31 13 Z" fill="var(--cap)"/><path d="M17 1 L31 13 L17 13 Z" fill="var(--cap-dark)"/><rect x="1" y="12" width="32" height="2" rx="1" fill="var(--cap-dark)"/></svg>

          <button class="board" type="button" aria-expanded="false" aria-controls="sign-menu" data-board data-autoclic>
            <span class="board__body" aria-hidden="true" data-board-body>
              <span class="board__face board__face--front"><span class="board__word">Menu</span><span class="shade" data-shade></span></span>
              <span class="board__face board__face--back"><span class="board__word">Close</span><span class="shade" data-shade></span></span>
            </span>
            <span class="sr" data-name>Open the menu</span>
          </button>

          <ul class="arms" id="sign-menu" data-arms inert>
            <li><a class="arm" href="#line-up" style="--paint: #d4532f; --ink: #fff6e6" data-arm><span class="arm__label">Line-up</span></a></li>
            <li><a class="arm" href="#tickets" style="--paint: #e9a823; --ink: #2b1a0b" data-arm><span class="arm__label">Tickets</span></a></li>
            <li><a class="arm" href="#camping" style="--paint: #2f6b4c; --ink: #fff6e6" data-arm><span class="arm__label">Camping</span></a></li>
            <li><a class="arm" href="#food" style="--paint: #2a7896; --ink: #fff6e6" data-arm><span class="arm__label">Food &amp; drink</span></a></li>
            <li><a class="arm" href="#getting-here" style="--paint: #6d3f7c; --ink: #fff6e6" data-arm><span class="arm__label">Getting here</span></a></li>
          </ul>
        </div>
      </div>
      <p class="sr" aria-live="polite" data-live></p>
    </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.

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

What it does

On a white page stands the top of a wooden post with a pointed iron cap, and hanging in front of it a pale oak board with Menu burnt into the wood: it is the button. Under the pointer the board creaks on its nail and the letters bounce. Pressed, the board spins round the post on a spring, shows its thickness and stops on its back, which reads Close; meanwhile the post slides down out of the board with its point first, goes a little too far, comes back and settles, and the whole sign sways from the top. As the end of the post goes past an arrow, the arrow is flung out round the post, one to the right and one to the left: it arrives edge-on, overshoots with its tip towards you, comes back and wobbles less and less, and its wobble runs along the post into the arrows next to it. The arrows are painted boards (Line-up red, Tickets yellow, Camping green, Food & drink blue, Getting here purple), with the grain of the wood showing under the paint, an iron strap on the post and the light changing on their faces as they turn. Brushing one with the pointer pushes it back, the harder the faster the hand goes, and it swings back into place with its letters bouncing. The one you are on reads You're here. Chosen, an arrow flips right over on its long side and nudges its neighbours; then the arrows fold back against the post from the bottom, the post slides up and the board turns back to Menu.

How it works

Boards made of slices

Every board is an element with transform-style: preserve-3d and five flat slices inside: the painted face in front at translateZ(3.5 units), three slices of bare wood at -2.2, 0 and 2.2, and the back face turned round. Head-on only the face shows; as the board turns, the slices in between peek out one behind the other and make its thickness. The arrow shape is one clip-path shared by all the slices; the back of an arrow is turned with rotateX(180deg), so when the chosen arrow flips over its long side the words on the back read the right way up.

The post sliding out

The post is a window with overflow: hidden, as tall as the script says, holding the whole length of wood pinned to its bottom: as the window grows, the wood comes down out of the board point first, like a telescopic pole. The length rides a damped spring (POLE_SPRING) that overshoots and comes back; when the post hits its stop, the SWAY swing rocks the whole scene with rotateZ from the top, on a very slow, lightly damped spring.

Arrows flung out by the post

Every arrow has an angle round the post (90 degrees is folded edge-on behind the post, 0 is out) and turns with rotateY round the edge of the post, to the left or to the right (the sign flips for the left-hand arrows). There is no fixed delay: every frame the script checks where the end of the post is, and when it goes past the middle of an arrow it gives the arrow the speed FLING outwards. So the cascade really follows the post, even when it is closed halfway.

Coupled springs along the post

Each arrow's torsion spring (ARM_SPRING) is lightly damped, so the arrow overshoots and wobbles. On top of that every arrow feels how far its neighbours are off their rest (COUPLING): it is a chain of coupled oscillators, and the wobble runs up and down the post. The physics takes four small steps a frame, with the step capped at 1/30 of a second, and nothing that has not changed is redrawn.

Light, push and bouncing letters

The normal of every face is worked out from the rotateY and rotateX angles, and its light is the dot product with a light from the upper left (Lambert): a dark veil over the face takes that as its opacity. The pointer entering an arrow pushes it back in proportion to its speed (BRUSH); with a finger the push comes on the touch. Each arrow's speed drives the BNCE axis of Shantell Sans, a variable font whose letters bounce up and down.

Values to change

NameFileDefaultWhat it changes
POLE_SPRINGscript.js[150, 0.42]The post sliding out: how hard it pulls to its length, and how damped it is (1 = no bounce).
ARM_SPRINGscript.js[210, 0.26]The arrows' torsion springs round the post: stiffness and damping.
FLINGscript.js620How fast an arrow is flung out when the post reaches it, in degrees a second.
COUPLINGscript.js70How much of an arrow's wobble passes to the next ones through the post.
BRUSHscript.js1How hard the pointer pushes an arrow it brushes past.
SPINscript.js[130, 0.34]The board's spin from Menu to Close: stiffness and damping.
FOLD_STAGGERscript.js0.055The seconds between one arrow folding back and the next, when closing.
SWAYscript.js1.4How far the whole sign sways when the post lands, in degrees.

Try them in the Playground

When to use it

For the menu of an outdoor site: a festival, a campsite, a park, a mountain hut, a walking or cycling trail, a farm. From three to six short sections (a dozen letters at most), each with its own colour; with more the post gets too long for the screen.

On phones

The whole drawing scales to the width of the screen: at 390 pixels an arrow is about 155 long and the open sign fits without spilling over. A finger touching an arrow gives it the same push as the pointer brushing past, and the tap chooses it.

With reduced motion

No spinning and no wobbling: the board changes face behind a short fade, and the post with its arrows appears or disappears already in place, fading. The arrows do not swing under the pointer and choosing does not flip them.

Pitfalls

  • With the left-hand arrows turned 180 degrees round the post, they ended up behind it: every arrow has its shape already facing its own side, with the pivot on the edge of the post, and turns with a rotateY of opposite sign; so they all stay in front of the post and all fold back edge-on.
  • With every arrow's spring coupled, an arrow just flung out (very far from its rest) pulled out the folded neighbours before the post had reached them: the coupling only works between arrows in the same state, out or folded.
  • A back face turned with rotateY(180deg) was fine for the swings round the post but showed upside-down words when the chosen arrow flips over its long side: the back is turned with rotateX(180deg), since that is the turn that shows it.

Libraries

GSAP 3.15.0, Shantell Sans (font variabile) 5.3.0.

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

Testing

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