Skip to content
animorio
IT
All components

The jack-in-the-box menu

Menus, Physics, SVG, HoverGSAP, CSS onlyAdvancedCompressed weight: Demo 14 KBGSAP 28 KBFreeDownload the zip

The menu of a toy shop as a lithographed tin jack-in-the-box: closed, it is the button, and under the pointer the crank turns on its own and the lid lifts a crack, as if something inside were pushing. Pressed, the crank winds a full turn while the box trembles, the lid bangs open against its stop and the menu card shoots out on a steel spring: it comes out narrow through the mouth of the box, widens, overshoots, stretches and sways on the spring, with a pleated ruff under it. The spring is a coil drawn again every frame along an axis that bends. The pointer passing over a section bats the card, which can also be grabbed and flicked; once a section is chosen, the card is pushed back down into the box and the lid snaps shut.

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 jack-in-the-box menu</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 menu of a toy shop as a lithographed tin jack-in-the-box. Closed, the box is the button: under the pointer
         its crank turns a little and the lid lifts a crack, as if something inside were pushing. Pressed, the crank
         winds a full turn, the lid bangs open and the menu card shoots out on a steel spring, overshoots, and wobbles
         on it. The spring is a coil drawn again every frame along a bent axis; the card can be grabbed and flicked.
         Choosing a section pushes the card back into the box and the lid snaps shut. -->
    <nav class="jack" aria-label="Toy shop" data-jack>
      <div class="jack__frame" data-frame>
        <!-- behind: the shadow on the table, the inside of the box and the lid, drawn by the script -->
        <svg class="jack__back" data-back aria-hidden="true">
          <defs>
            <radialGradient id="jack-shadow" cx="50%" cy="50%" r="50%">
              <stop offset="0" stop-color="#1b0f0a" stop-opacity="0.26"/>
              <stop offset="1" stop-color="#1b0f0a" stop-opacity="0"/>
            </radialGradient>
            <linearGradient id="jack-inside" x1="0" y1="0" x2="0.3" y2="1">
              <stop offset="0" class="stop-inside-lit"/>
              <stop offset="1" class="stop-inside"/>
            </linearGradient>
            <linearGradient id="jack-lining" x1="0" y1="0" x2="1" y2="1">
              <stop offset="0" class="stop-lining-lit"/>
              <stop offset="0.55" class="stop-lining"/>
              <stop offset="1" class="stop-lining-dark"/>
            </linearGradient>
          </defs>
          <ellipse class="jack__shadow" data-shadow fill="url(#jack-shadow)"/>
          <path class="box-inside" data-inside fill="url(#jack-inside)"/>
          <path class="box-rim" data-rim/>
          <path class="lid-under" data-lid-under fill="url(#jack-lining)"/>
          <path class="lid-top" data-lid-top/>
          <path class="lid-lip" data-lid-lip/>
          <path class="lid-line" data-lid-line/>
        </svg>

        <!-- the jack: the spring and the card. Everything under the front edge of the box is inside it, so hidden -->
        <div class="jack__out" data-out>
          <svg class="spring" data-spring-svg aria-hidden="true">
            <path class="spring__back" data-coil-back/>
            <path class="spring__edge" data-coil-edge/>
            <path class="spring__front" data-coil-front/>
            <path class="spring__shine" data-coil-shine/>
          </svg>

          <div class="card" data-card>
            <div class="card__body">
              <ul class="card__list" id="jack-menu" data-list inert>
                <li><a class="item" href="#shop" data-item><span class="item__label">Shop toys</span></a></li>
                <li><a class="item" href="#wind-ups" data-item><span class="item__label">Wind-ups</span></a></li>
                <li><a class="item" href="#puppets" data-item><span class="item__label">Puppets</span></a></li>
                <li><a class="item" href="#gift-cards" data-item><span class="item__label">Gift cards</span></a></li>
                <li><a class="item" href="#visit" data-item><span class="item__label">Visit us</span></a></li>
              </ul>
            </div>
            <!-- the pleated ruff round the jack's neck, where the spring holds the card -->
            <svg class="ruff" viewBox="0 0 120 30" aria-hidden="true" data-ruff></svg>
          </div>
        </div>

        <!-- in front: the tin box, which is the button -->
        <button class="box" type="button" aria-expanded="false" aria-controls="jack-menu" data-box data-autoclic>
          <svg class="box__art" data-box-art aria-hidden="true">
            <defs>
              <pattern id="jack-diamonds" width="28" height="36" patternUnits="userSpaceOnUse">
                <rect width="28" height="36" class="tin-red"/>
                <path d="M14 0 L28 18 L14 36 L0 18 Z" class="tin-red-dark"/>
              </pattern>
              <pattern id="jack-stripes" width="12" height="12" patternUnits="userSpaceOnUse" patternTransform="skewY(-46.7)">
                <rect width="12" height="12" class="tin-blue"/>
                <rect width="6" height="12" class="tin-blue-dark"/>
              </pattern>
              <linearGradient id="jack-side-shade" x1="0" y1="0" x2="1" y2="0">
                <stop offset="0" stop-color="#000" stop-opacity="0.08"/>
                <stop offset="1" stop-color="#000" stop-opacity="0.34"/>
              </linearGradient>
              <linearGradient id="jack-front-shade" x1="0" y1="0" x2="0" y2="1">
                <stop offset="0" stop-color="#fff" stop-opacity="0.18"/>
                <stop offset="0.4" stop-color="#fff" stop-opacity="0"/>
                <stop offset="1" stop-color="#000" stop-opacity="0.16"/>
              </linearGradient>
            </defs>
            <g data-faces></g>
            <g data-crank></g>
          </svg>
          <span class="box__plate" aria-hidden="true">
            <span class="box__words" data-words><span class="box__word">Menu</span><span class="box__word">Close</span></span>
          </span>
          <span class="sr" data-name>Open the menu</span>
        </button>
      </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.

36 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 jack-in-the-box menu” from Animorio and fit it into my site

What it does

On a white page stands a tin box seen a little from above and from the right: red with diamonds in front, a striped blue side, a yellow lid, yellow bands at the top and the foot, a crank with a wooden knob on the side and a cream plate reading Menu. It is the button. With the pointer on it the crank turns slowly on its own and the lid lifts a finger's width and drops back on its spring. Pressed, the word on the plate rolls over to Close, the crank winds a full turn, faster and faster, and the box trembles more and more; then the lid flies open, hits its stop and bounces, and only when its edge has gone back behind the card does the card leave. It comes out of the mouth of the box narrow, widens as it clears it, rises higher than where it will stop, stretches on the way and comes back down on the steel spring, which can be seen coming out of the box with its near turns bright and its far turns dark. As soon as it clears the rim the card gets a push to the side and sways, with the spring bending under it and the card nodding a beat behind, like a head. On the card are the five sections of the shop (Shop toys, Wind-ups, Puppets, Gift cards, Visit us), and the one you are on reads You're here. Passing the pointer over a section makes its letters hop one after the other and land with a little bounce, and the card is batted the way the hand is going. The card can be grabbed and pulled: it is held on a rubber band, and let go it sways again. Choosing a section makes its letters hop higher, then the card is pushed straight down into the box, the lid snaps shut, the box jolts and the plate reads Menu again.

How it works

A box in oblique projection

Everything is drawn in units, with a point (x, y, z) landing on the screen at (x + z·0.32, y − z·0.34): so the front, the right side and the top show without a real perspective. The faces of the box are SVG drawn once, with the diamonds and the stripes as patterns (the side one skewed with a skewY of the side's own angle). The lid is drawn again by the script: each of its points turns round the hinge at the back, and past 19 degrees its inner face of bare tin shows, because that is where its normal stops facing the viewer.

Inside the box, cut at the rim

The spring and the card live in a layer with a clip-path cutting everything below the front edge of the box: in oblique projection, a point half way back and under the plane of the lid shows through the mouth only above that line, so the cut is exact. The card is wider than the mouth: on its way out it is squeezed to 80 per cent and widens as the ruff clears the rim. The card only leaves once the lid has passed 62 degrees, when its front edge has gone behind the card: before that it would go through it.

The spring that throws

The card's height above the rim rides a damped spring (POP) with its speed capped at LAUNCH: the huge force of the compressed spring takes it straight to full speed, it runs out of the box and around its resting place the spring brakes it, throws it past by about LAUNCH over the angular frequency and brings it back down. The speed stretches the card upright and narrows it by as much as keeps its area. To close, the spring lets go and the card falls with a fixed acceleration, with the sideways spring made much stiffer so it goes in straight.

Sway and nod

The sideways shift of the top of the spring is another lightly damped oscillator (WOBBLE), and the card leans LEAN degrees per unit like the tip of a bent spring. On top of that the card has its own angle, the nod, on a quick spring driven backwards by the acceleration of the top: when the top jumps right, the card lags and then catches up. The pointer entering a section gives the top a speed in proportion to its own (BAT); grabbed, the card follows the pointer on a rubber band (a hyperbolic tangent) and let go it leaves with the speed of the hand.

The coil drawn again

The axis of the spring is a cubic Bézier curve leaving the floor of the box straight up and reaching the ruff tilted with the card. Along it the script puts 26 points a turn: each point moves sideways by the radius times the sine of its angle, and along the axis by the radius times the cosine times 0.3, since each turn, seen a little from above, is an ellipse with its near side lower. The sign of the cosine says whether the wire is in front or behind: the far runs go into a dark path underneath, the near ones into three paths on top (dark edge, steel, glint). Nothing is redrawn while the card and the spring are still.

Values to change

NameFileDefaultWhat it changes
POPscript.js[210, 0.2]The spring the card rides out on: how hard it pulls to its place, and how damped it is (1 = no bounce).
LAUNCHscript.js1500The top speed of the card jumping out, in units a second: the higher, the further it overshoots.
WOBBLEscript.js[46, 0.09]The card's sway from side to side on the spring: stiffness and damping.
LEANscript.js0.38How far the card leans as it sways, in degrees for every unit it moves.
WINDscript.js0.42The seconds the crank winds before the lid flies open.
LIDscript.js[190, 0.24]The lid flying open and shutting: stiffness and damping.
BATscript.js1How hard the pointer passing over a section pushes the card.
HOPscript.js0.3How high the letters of a section hop under the pointer, in parts of their size.

Try them in the Playground

When to use it

For the menu of a site that wants to make people smile: a toy shop, a children's party, a circus, a cake shop, a games app, a page of surprises or presents. From three to six short sections (a dozen letters at most), set on the card one under the other.

On phones

The whole drawing scales to the width and the height of the screen: at 390 pixels the card is about 215 wide and the open box fits without spilling over. With touch there is no crank turning on its own: a tap opens it straight away. The card can be grabbed and flicked with a finger too, and a tap on a section chooses it.

With reduced motion

No jumping and no swaying: pressed, the lid is already open and the card fades in in its place; closing, it fades out and the lid is already shut. The crank does not turn, the letters do not hop and the card cannot be grabbed; the section under the pointer only changes colour.

Pitfalls

  • The card left together with the lid and, in the first frames, went right through it: the lid's front edge is in front of the card until the angle passes 60 degrees. The card waits in a state of its own until the lid has passed 62 degrees.
  • The sideways push given at launch made the card come out of the mouth askew, turned round a point still inside the box: the push comes when the ruff clears the rim, and the lean only reaches its full size after the first 40 units out of the box.
  • Close sat under the plate with a translateY(110%) in the CSS, and GSAP, reading it as pixels, added its own yPercent on top: the word stayed out of sight. At start the script takes it over with y: 0 and yPercent: 110.
  • The outer face of the lid was chosen with tan(angle) under 0.34, which is also true past 90 degrees where the tangent is negative: the yellow outside showed instead of the bare tin inside. Now the angle is compared with arctan(0.34).

Libraries

GSAP 3.15.0, Fraunces (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 14 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