Skip to content
animorio
IT
All components

The shop sign tab bar

Navigation, Physics, HoverCSS only, GSAPAdvancedCompressed weight: Demo 12 KBGSAP 28 KBFreeDownload the zip

The bottom bar of a neighbourhood café's app: over the chosen tab hangs a small enamel sign with its name, on two cords from a thin brass rail. Changing tab, the hooks run along the rail and the sign follows late: it trails, tilts, swings past and settles like a real rigid body, then the icon under it hops and gives it a knock.

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 shop sign tab bar</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/full.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the tab bar of a neighbourhood café's app, on a cream board. Along its top runs a thin brass rail, and from
         two hooks on it hangs a small enamel sign with the name of the tab you are on, like the plate over a shop
         door. Choosing another tab, the hooks run along the rail and the sign follows on its two cords: it trails,
         tilts, swings past and settles, and the icon under it hops. The sign is a rigid plate worked out by the
         script on every frame -->
    <nav class="bar" aria-label="Main" data-bar>
      <button class="tab" type="button" data-tab data-autoclic>
        <span class="tab__lift" data-lift>
          <svg class="tab__icon" viewBox="0 0 24 24" aria-hidden="true">
            <path class="tab__fill" d="M5.4 4.6h13.2l1.6 4.6H3.8z"/>
            <path d="M3.8 9.2 5.4 4.6h13.2l1.6 4.6M8.7 4.6l-.8 4.6M12 4.6v4.6M15.3 4.6l.8 4.6"/>
            <path d="M3.8 9.2a2.05 2.05 0 0 0 4.1 0a2.05 2.05 0 0 0 4.1 0a2.05 2.05 0 0 0 4.1 0a2.05 2.05 0 0 0 4.1 0"/>
            <path d="M5.3 12.4v7.4h13.4v-7.4M10.2 19.8v-4.4h3.6v4.4"/>
          </svg>
        </span>
        <span class="sr" data-name>Home</span>
      </button>
      <button class="tab" type="button" data-tab>
        <span class="tab__lift" data-lift>
          <svg class="tab__icon" viewBox="0 0 24 24" aria-hidden="true">
            <path class="tab__fill" d="M5.2 10.2h11.2v3a5.2 5.2 0 0 1-5.2 5.2h-.8a5.2 5.2 0 0 1-5.2-5.2z"/>
            <path d="M5.2 9.4h11.2v3.8a5.2 5.2 0 0 1-5.2 5.2h-.8a5.2 5.2 0 0 1-5.2-5.2zM16.4 10.6h1.2a2.3 2.3 0 0 1 0 4.6h-1.5M3.8 20.6h14"/>
            <path d="M8.9 3.4c-.9 1.1.9 2.1 0 3.4M12.6 3.4c-.9 1.1.9 2.1 0 3.4" data-steam/>
          </svg>
        </span>
        <span class="sr" data-name>Menu</span>
      </button>
      <button class="tab" type="button" data-tab data-autoclic>
        <span class="tab__lift" data-lift>
          <svg class="tab__icon" viewBox="0 0 24 24" aria-hidden="true">
            <path class="tab__fill" d="M5.6 8.4h12.8l-.9 11.2a1 1 0 0 1-1 .9H7.5a1 1 0 0 1-1-.9z"/>
            <path d="M5.6 8.4h12.8l-.9 11.2a1 1 0 0 1-1 .9H7.5a1 1 0 0 1-1-.9zM9.2 10.8V7.4a2.8 2.8 0 0 1 5.6 0v3.4"/>
          </svg>
        </span>
        <span class="sr" data-name>Orders</span>
      </button>
      <button class="tab" type="button" data-tab>
        <span class="tab__lift" data-lift>
          <svg class="tab__icon" viewBox="0 0 24 24" aria-hidden="true">
            <path class="tab__fill" d="M7 4.4h10a.6.6 0 0 1 .6.6v15.2l-5.6-4-5.6 4V5a.6.6 0 0 1 .6-.6z"/>
            <path d="M7 4.4h10a.6.6 0 0 1 .6.6v15.2l-5.6-4-5.6 4V5a.6.6 0 0 1 .6-.6z"/>
          </svg>
        </span>
        <span class="sr" data-name>Saved</span>
      </button>
      <button class="tab" type="button" data-tab data-autoclic>
        <span class="tab__lift" data-lift>
          <svg class="tab__icon" viewBox="0 0 24 24" aria-hidden="true">
            <path class="tab__fill" d="M12 4.4a3.7 3.7 0 1 0 0 7.4a3.7 3.7 0 1 0 0-7.4z"/>
            <path d="M12 4.4a3.7 3.7 0 1 0 0 7.4a3.7 3.7 0 1 0 0-7.4zM4.8 20.2c.9-3.7 3.7-5.7 7.2-5.7s6.3 2 7.2 5.7"/>
          </svg>
        </span>
        <span class="sr" data-name>Profile</span>
      </button>

      <!-- the brass rail along the top, and everything that hangs from it: the sign's shadow on the board, the two
           cords, the two hooks and the sign. None of it takes the taps -->
      <span class="rail" aria-hidden="true" data-rail></span>
      <span class="hanger" aria-hidden="true" data-hanger>
        <span class="shade" data-shade></span>
        <svg class="cords">
          <g class="cords__shade" transform="translate(1 2.4)"><path data-cord-shade/><path data-cord-shade/></g>
          <g class="cords__line"><path data-cord/><path data-cord/></g>
        </svg>
        <span class="hook" data-hook></span>
        <span class="hook" data-hook></span>
        <span class="sign" data-sign>
          <span class="sign__plate">
            <span class="sign__rule"></span>
            <span class="sign__words" data-words></span>
            <span class="sign__side" data-side></span>
            <span class="sign__gloss" data-gloss></span>
          </span>
          <span class="sign__clip" data-clip></span>
          <span class="sign__clip sign__clip--right" data-clip></span>
        </span>
      </span>
    </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.

26 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 shop sign tab bar” from Animorio and fit it into my site

What it does

On a white background, alone in the middle, a cream bar like a painted board, 72 pixels tall, with five line icons: a shop with a scalloped awning, a steaming cup, a paper bag, a bookmark and a person, that is Home, Menu, Orders, Saved and Profile. Along the top edge runs a thin brass rail, held by a bracket at each end, and over Menu hangs a small oxblood enamel sign, with a fine light border painted round it and the name MENU in serif capitals, hung on two dark cords from two brass hooks that grip the rail. Under the sign the cup is dark and filled with a wash of the sign's colour; the other icons are a warm grey. On arrival the bar rises and the sign is hung up: it shows a little higher and askew, its cords slack, drops, the lower cord goes taut first and it swings until it stops. Choosing another tab, the two hooks run along the rail, the front one a few pixels ahead: the cords bend back and the sign trails behind, tilting, then, as the hooks brake, it swings past, comes back and settles. Halfway the name changes letter by letter, the old ones rising away and the new ones coming up from below, from the side it is heading to. As soon as the hooks stop the new icon darkens, gives a little hop that stretches it, knocks the sign's lower edge (which jumps, its cords going slack and bending for a moment) and lands with a squash. If the swinging sign comes down too far over an icon it passes, the icon gives way and tilts on the side it is pressed. With the mouse over another tab, or a finger down on it, the sign turns towards it in perspective: the far side narrows and darkens, the gloss on the enamel slides and the hooks shift slightly towards the tab. Clicking the tab already chosen gives the sign a flick: it swings, twists a little and the icon under it knocks it. From the keyboard, Tab turns the sign towards the focused tab, which has a ring round its icon; the arrows, Home and End choose.

How it works

A rigid plate of four points

The sign is its four corners, four points with a position and a speed. Six distance constraints keep them rigid: the four sides and the two diagonals. At every step the points move with their speed and gravity, then the constraints put the positions right (six rounds), and the new speed is the real displacement divided by the time: Verlet integration written with speeds, which takes knocks and pushes well. Steps of at most 1/240 of a second, frames of at most 1/30.

Two cords that pull and never push

Each cord is a constraint that only acts when taut: if the point it is tied to is further from the hook than the cord is long, the plate is pulled back. The tie point lies inside the plate, so it is a blend of three corners, and the correction is shared between them by their weight in the blend. The cords lean in a little (TOE): with parallel cords the plate would slide along level like a swing, this way it rocks as it swings, like a pendulum hung from a point above the rail.

The hooks and the damping

GSAP carries the hooks: one tween moves their middle with power2.inOut, another runs the front one a few pixels ahead and brings it back. Within a frame the hooks move a little at every step of the sums, so even at 30 frames a second the plate gets no jolts. The damping works on the plate's speed relative to the hooks': it slows the swinging, not the ride.

The twist and the drawing

Turning towards a tab is the twist of a sign hung on two strings (a bifilar pendulum), with its own spring: the stiffness comes from gravity, the distance between the cords, their length and the sign's width. The sign is a real HTML element (its text stays sharp) moved with translate, rotate and perspective plus rotateY; the cords are two SVG paths to the clips, worked out with the same perspective, straight when taut and bowed outwards when slack; the shadow on the board follows the plate.

Icons that feel the sign

When the sign arrives, the icon under it hops with a GSAP timeline; while it rises it is an obstacle the solver knows about: if the plate's lower edge is inside it, it is pushed up, and the plate jumps on its own. The other way round, if the swinging plate comes down on an icon, the icon is pushed down and tilted on the side pressed, and comes back on a spring. The tabs are buttons in a nav, the chosen one has aria-current="page"; when nothing moves any more the loop stops working and drawing.

Values to change

NameFileDefaultWhat it changes
STARTscript.js1The tab chosen when the page opens (0 is the first one).
TRAVELscript.js0.46The seconds the hooks take to reach the next tab (a far one takes a little more).
LEADscript.js5How many pixels the front hook runs ahead of the back one on the way: the sign tilts.
CORDscript.js7The length of the two cords, in pixels.
TOEscript.js3.5How far the cords lean in towards each other, in pixels: more, the sign rocks more as it swings (0, it slides along level).
GRAVITYscript.js2400How heavy the sign is, in pixels per second squared: more, quicker and smaller swings.
DAMPINGscript.js2.6How soon the swinging dies away: more, it stops sooner.
TURNscript.js30How many degrees the sign turns towards a tab under the mouse or the finger.
HOPscript.js6How high the icon hops when the sign arrives over it, in pixels (above about 5 it knocks it).

Try them in the Playground

When to use it

For the bottom bar of an app made for phones, with four or five sections, when the app has the feel of a shop: a café, a bookshop, a bakery, a corner store, a recipe app. Even standing still the sign says where you are, in place of the labels under the icons.

On phones

The bar is at most 420 pixels wide and on a phone narrows to the screen with a 16 pixel margin each side (358 on a 390 phone); it stays 72 tall and each tab is a target of about 69 by 72 pixels. The sign is 60 pixels wide and fits its column there too. With a finger down on a tab the sign turns towards it, as with the mouse over it, and the tap chooses it. Vertically the page still scrolls (touch-action: pan-y).

With reduced motion

No ride, no swinging, no twist and no hop: the sign, with its cords and hooks, fades off the old tab and back in, still and level, on the new one with its new name, while the icons light up and go out with a fade. On arrival the bar only fades in, with the sign already hung.

Pitfalls

  • At first the back hook set off a fixed time late: at the speeds of the ride the hooks drew further apart than the sign's clips, both cords pulled outwards and the plate was lifted up against the rail, its cords almost flat. Now the front hook runs a few pixels ahead at most (LEAD) and then falls back in.
  • Turning towards a tab, the perspective brings the clips seen on the screen closer: the cords came out longer than the distance, were drawn slack and curled up like hooks. Whether a cord is slack is now decided by the real plate; the twist only moves the point where the cord is drawn to.
  • Turning back halfway, the lead passed from one hook to the other and the hooks jumped. Now, when the direction changes, the middle of the ride shifts by the same amount: the same two hooks stay where they are, with the other one in front.
  • Halfway through the change of name the old letters and the new ones overlapped in the middle of the sign and read as a muddle: now the old ones leave faster and the new ones start a little later.
  • The focus ring round the icon touched the sign's lower edge: the rail and everything hanging from it moved up almost a pixel and the ring is drawn inside its box.
  • At rest the loop did not stop for four seconds: the solver leaves a motion of about a pixel a second, invisible. The threshold is now 3 pixels a second, that is a swing smaller than two tenths of a pixel.

Libraries

GSAP 3.15.0, Fraunces (font variabile) 5.3.0.

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

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 components