Skip to content
animorio
IT
All components

The jukebox menu

Menus, Hover, SVGGSAP, CSS onlyAdvancedCompressed weight: Demo 13 KBGSAP 28 KBFreeDownload the zip

In the rich style: the Menu button of a music bar is the top of a fifties jukebox; pressed, the whole jukebox opens under it, the glass slides up and the records stand in their rack. Every menu item is a title strip: choosing it, the mechanical arm takes its record, lays it on the turntable, the record spins, the needle drops and notes rise from the record.

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 jukebox 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 menu of a music bar: the Menu button is the top of a fifties jukebox; pressed, the whole jukebox opens
         under it, the glass slides up and the records stand in their rack. Pointing at a title strip lifts its record;
         choosing it, the mechanical arm takes the record to the turntable, the record spins, the needle drops and
         notes come out of the speaker grille. -->
    <nav class="juke" aria-label="Main" data-jukebox>
      <button class="juke-btn" type="button" aria-expanded="false" aria-controls="juke-cabinet" data-toggle>
        <svg class="juke-btn__icon" viewBox="0 0 44 44" aria-hidden="true">
          <defs>
            <linearGradient id="btn-chrome" x1="0" y1="0" x2="0" y2="1">
              <stop offset="0" stop-color="#ffffff"/>
              <stop offset="0.45" stop-color="#9aa3ad"/>
              <stop offset="0.55" stop-color="#e9edf1"/>
              <stop offset="1" stop-color="#6f7882"/>
            </linearGradient>
            <clipPath id="btn-arch"><path d="M7 41 V 20 A 15 15 0 0 1 37 20 V 41 Z"/></clipPath>
          </defs>
          <path class="juke-btn__body" d="M7 41 V 20 A 15 15 0 0 1 37 20 V 41 Z"/>
          <g clip-path="url(#btn-arch)">
            <path class="juke-btn__band juke-btn__band--red" d="M7 41 V 20 A 15 15 0 0 1 37 20 V 41 H 33.5 V 20 A 11.5 11.5 0 0 0 10.5 20 V 41 Z"/>
            <path class="juke-btn__band juke-btn__band--cream" d="M10.5 41 V 20 A 11.5 11.5 0 0 1 33.5 20 V 41 H 31 V 20 A 9 9 0 0 0 13 20 V 41 Z"/>
            <path class="juke-btn__glass" d="M13 30 V 20 A 9 9 0 0 1 31 20 V 30 Z"/>
            <g class="juke-btn__disc">
              <circle cx="22" cy="24" r="6.4" class="juke-btn__vinyl"/>
              <circle cx="22" cy="24" r="2.4" class="juke-btn__label"/>
            </g>
            <rect class="juke-btn__strips" x="13" y="30" width="18" height="3.4" rx="0.6"/>
            <g class="juke-btn__grille">
              <rect x="13" y="34.6" width="18" height="6.4" rx="1"/>
              <path d="M15.5 34.6 V 41 M18.5 34.6 V 41 M21.5 34.6 V 41 M24.5 34.6 V 41 M27.5 34.6 V 41"/>
            </g>
            <rect class="juke-btn__sweep" x="-12" y="0" width="7" height="44" transform="skewX(-18)"/>
          </g>
          <path class="juke-btn__rim" d="M7 41 V 20 A 15 15 0 0 1 37 20 V 41"/>
        </svg>
        <span class="juke-btn__word" data-word>Menu</span>
      </button>

      <div class="cabinet" id="juke-cabinet" data-cabinet inert>
        <div class="cabinet__body" data-body>
          <!-- the arch around the window: three bands of coloured plastic with light running through them -->
          <div class="arch">
            <div class="arch__band arch__band--red"><span class="arch__light" data-light></span></div>
            <div class="arch__band arch__band--cream"><span class="arch__light" data-light></span></div>
            <div class="arch__band arch__band--mint"><span class="arch__light" data-light></span></div>
            <div class="window" data-window>
              <svg class="mech" viewBox="-20 0 440 200" preserveAspectRatio="xMidYMid meet" aria-hidden="true" data-mech>
                <defs>
                  <linearGradient id="chrome-h" x1="0" y1="0" x2="0" y2="1">
                    <stop offset="0" stop-color="#fbfdff"/>
                    <stop offset="0.38" stop-color="#a7b0ba"/>
                    <stop offset="0.52" stop-color="#eef2f6"/>
                    <stop offset="1" stop-color="#5d6670"/>
                  </linearGradient>
                  <linearGradient id="chrome-v" x1="0" y1="0" x2="1" y2="0">
                    <stop offset="0" stop-color="#5d6670"/>
                    <stop offset="0.4" stop-color="#eef2f6"/>
                    <stop offset="0.6" stop-color="#a7b0ba"/>
                    <stop offset="1" stop-color="#fbfdff"/>
                  </linearGradient>
                  <radialGradient id="vinyl" cx="0.5" cy="0.5" r="0.5">
                    <stop offset="0" stop-color="#1b1a1d"/>
                    <stop offset="0.8" stop-color="#0c0b0d"/>
                    <stop offset="1" stop-color="#232127"/>
                  </radialGradient>
                  <linearGradient id="sheen" x1="0" y1="0" x2="1" y2="0">
                    <stop offset="0" stop-color="#ffffff" stop-opacity="0"/>
                    <stop offset="0.5" stop-color="#ffffff" stop-opacity="0.28"/>
                    <stop offset="1" stop-color="#ffffff" stop-opacity="0"/>
                  </linearGradient>
                  <radialGradient id="felt" cx="0.5" cy="0.4" r="0.6">
                    <stop offset="0" class="stop-cherry-dark"/>
                    <stop offset="1" stop-color="#2a070b"/>
                  </radialGradient>
                  <linearGradient id="plinth" x1="0" y1="0" x2="0" y2="1">
                    <stop offset="0" stop-color="#2a2427"/>
                    <stop offset="1" stop-color="#0d0b0c"/>
                  </linearGradient>
                  <linearGradient id="plinth-top" x1="0" y1="0" x2="1" y2="1">
                    <stop offset="0" stop-color="#3a3236"/>
                    <stop offset="0.6" stop-color="#141113"/>
                    <stop offset="1" stop-color="#241e21"/>
                  </linearGradient>
                  <linearGradient id="tray" x1="0" y1="0" x2="0" y2="1">
                    <stop offset="0" class="stop-cherry"/>
                    <stop offset="1" class="stop-cherry-dark"/>
                  </linearGradient>
                  <clipPath id="disc-clip"><circle r="30"/></clipPath>
                </defs>

                <!-- the turntable: a lacquered drum with a chrome band, the platter with its felt mat and chrome rim, the spindle -->
                <g class="table" transform="translate(22 0)">
                  <ellipse class="table__shadow" cx="318" cy="170" rx="66" ry="12"/>
                  <path class="table__side" d="M258 150 V 162 A 60 15 0 0 0 378 162 V 150 Z" fill="url(#plinth)"/>
                  <path class="table__band" d="M258 157 A 60 15 0 0 0 378 157"/>
                  <ellipse class="table__top" cx="318" cy="150" rx="60" ry="15" fill="url(#plinth-top)"/>
                  <path class="table__edge" d="M258 150 A 60 15 0 0 0 378 150"/>
                  <ellipse class="table__rim" cx="318" cy="148" rx="52" ry="12.8"/>
                  <ellipse class="table__mat" cx="318" cy="147.4" rx="49.4" ry="11.8" fill="url(#felt)"/>
                  <path class="table__mat-rings" d="M276 147.4 A 42 9.8 0 0 1 360 147.4 M286 147.4 A 32 7.4 0 0 1 350 147.4"/>
                  <rect class="table__spindle" x="316.8" y="139" width="2.4" height="8.6" rx="1"/>
                </g>

                <!-- the rack: chrome dividers behind, the records standing in it, the lacquered tray in front -->
                <g class="rack">
                  <g class="rack__comb" data-comb></g>
                </g>
                <g class="records" data-records></g>
                <g class="tray">
                  <rect class="tray__front" x="4" y="126" width="266" height="22" rx="3" fill="url(#tray)"/>
                  <rect x="4" y="123" width="266" height="4.6" rx="2" fill="url(#chrome-h)"/>
                  <path class="tray__line" d="M12 139 H 262"/>
                  <path class="tray__shine" d="M14 130 H 120"/>
                  <path class="tray__shadow" d="M6 150 H 268"/>
                  <g data-tabs></g>
                </g>

                <!-- the tonearm: a chrome post, the arm hinged on it, the headshell with the needle -->
                <g class="arm" data-tonearm>
                  <g class="arm__swing" data-swing>
                    <path class="arm__tube" d="M0 0 L 30 -2.4"/>
                    <path class="arm__tube-shine" d="M0 -1 L 30 -3.2"/>
                    <g class="arm__head" data-head>
                      <path class="arm__shell" d="M28 -6 L 40 -5 L 40 1.4 L 28 1 Z"/>
                      <path class="arm__needle" d="M37 1.2 L 37.6 4.6"/>
                    </g>
                    <rect class="arm__weight" x="-11" y="-4.6" width="8" height="7.4" rx="1.6" fill="url(#chrome-v)"/>
                  </g>
                  <ellipse class="arm__base" cx="0" cy="3" rx="7" ry="2.6" fill="url(#chrome-h)"/>
                  <rect class="arm__post" x="-3" y="-3" width="6" height="7" rx="1.4" fill="url(#chrome-v)"/>
                  <ellipse class="arm__cap" cx="0" cy="-3" rx="3" ry="1.2"/>
                </g>

                <!-- the gantry: a chrome rail across the top, a carriage that runs on it, a rod that drops, a claw -->
                <g class="gantry">
                  <rect class="gantry__rail" x="-6" y="13" width="412" height="5.2" rx="2" fill="url(#chrome-h)"/>
                  <rect class="gantry__bracket" x="-8" y="6" width="5" height="16" rx="1.4" fill="url(#chrome-v)"/>
                  <rect class="gantry__bracket" x="403" y="6" width="5" height="16" rx="1.4" fill="url(#chrome-v)"/>
                  <g data-carriage>
                    <rect class="gantry__rod" x="-1.8" y="22" width="3.6" height="10" fill="url(#chrome-v)" data-rod/>
                    <g data-claw>
                      <path class="gantry__finger" d="M0 0 C -6 1 -9 5 -8.4 10" data-finger-l/>
                      <path class="gantry__finger" d="M0 0 C 6 1 9 5 8.4 10" data-finger-r/>
                      <rect class="gantry__knuckle" x="-4" y="-2.6" width="8" height="4.4" rx="1.4" fill="url(#chrome-h)"/>
                    </g>
                    <rect class="gantry__car" x="-13" y="10" width="26" height="12" rx="3" fill="url(#chrome-h)"/>
                    <rect class="gantry__lip" x="-13" y="19" width="26" height="1.6"/>
                  </g>
                </g>
              </svg>
              <!-- the glass in front of the records: it slides up into the arch when the jukebox opens -->
              <div class="window__glass" data-glass></div>
            </div>
          </div>

          <!-- the title strips: the real links of the menu, one for every record -->
          <ul class="strips" data-strips>
            <li><a class="strip" href="#menu" data-item style="--tone: var(--cherry)"><span class="strip__band"></span><span class="strip__word">Menu</span></a></li>
            <li><a class="strip" href="#drinks" data-item style="--tone: var(--mint)"><span class="strip__band"></span><span class="strip__word">Drinks</span></a></li>
            <li><a class="strip" href="#live" data-item style="--tone: var(--gold)"><span class="strip__band"></span><span class="strip__word">Live music</span></a></li>
            <li><a class="strip" href="#book" data-item style="--tone: var(--sky)"><span class="strip__band"></span><span class="strip__word">Book a table</span></a></li>
            <li><a class="strip" href="#find" data-item style="--tone: var(--tangerine)"><span class="strip__band"></span><span class="strip__word">Find us</span></a></li>
          </ul>

          <!-- the speaker grille, with the chrome plate that says what is playing -->
          <div class="grille" data-grille>
            <span class="grille__plate"><span class="grille__brand">Jive</span><span class="grille__now" data-now aria-live="polite"></span></span>
          </div>
        </div>
        <svg class="notes" aria-hidden="true" data-notes></svg>
      </div>
    </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.

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

What it does

On a black background a cherry red pill button with a chrome rim: inside it there is the top of a jukebox drawn small, with the plastic arch, the glass, a record, the title strips and the grille. Pointing at it a light runs over the arch and the little record turns. Pressing it, the word turns into Close and the jukebox of Jive, a music bar, opens under it: the arch of three bands of plastic, red, cream and mint, with stripes of light running through them; the glass with its reflections sliding up and vanishing into the arch; behind it, on a glossy black back warmed by a golden light, five records dropping upright into the chrome rack, each with a label of its own colour, the turntable with its red felt platter, the tonearm on its rest and, at the top, a chrome rail with a carriage and a claw. Under it there are the title strips, the real items of the menu: Menu, Drinks, Live music, Book a table, Find us, each with a band in the colour of its record, and at the bottom the speaker grille with its chrome plate. Pointing at a strip lifts its record a little. Choosing it, the carriage runs over the record, the claw drops, takes it and lifts it, carries it over the turntable and while it comes down the record lies down flat on the platter; the record starts spinning with the light standing still on its grooves, the tonearm swings over it and the needle drops, the plate on the grille says the title and notes rise from the record, swaying in the dark behind the glass. Choosing another title, the needle lifts, the record slows down, the claw puts it back in its place and goes to fetch the new one.

How it works

A mechanism made of a few numbers

Everything that moves inside the jukebox is a 400 by 200 SVG drawn again on every frame from a few numbers: where the carriage is on the rail, how long the rod is, how closed the claw is, the angle of the tonearm, and for every record its position, how far it lies down and how far it has turned. GSAP animates only those numbers; one function turns them into transform attributes.

The record in the claw

While a record is in the claw its position is not animated: it is worked out from the claw. Its centre sits one radius under the fingertips, times how far the record lies down; so, while the rod comes down over the platter and the record goes from upright (vertical scale 1) to lying down (0.3), its edge stays held between the fingers and the record lands on the felt exactly where it should.

Light that does not turn

Every record has two groups: the one that turns (vinyl, grooves, label with its print and hole) and on top, still, two opposite wedges of light clipped to the circle. When the record spins the print on the label runs round and the light stays put, like on a real record. Lying down, the whole record is squashed vertically and the turn becomes a turn on an ellipse.

The lights in the arch and the glass

The arch is three bands of plastic one inside the other with the same curve; through each one runs a stripe of light, a repeating gradient moved by background-position by exactly one period, so the loop never jumps. The glass is a sheet of reflections that slides up and vanishes into the arch when it opens; the jukebox unrolls from the top with a clip-path and settles with an elastic.

Notes and real links

The items are real links, the title strips, reachable with the Tab key and the arrows; the button has aria-expanded, the closed jukebox is inert, Esc closes and gives the focus back to the button, and the plate on the grille says the title in an aria-live region. While the record plays, now and then a note is born (an eighth note or two joined ones) drawn in an SVG over the jukebox: it starts from the record on the platter (the point is turned from the mechanism’s coordinates into the page’s with getScreenCTM), rises swaying in the dark behind the glass, where it covers no words, tilts and fades.

Values to change

NameFileDefaultWhat it changes
SPEEDscript.js1How fast the mechanism moves (1 is normal).
RPMscript.js45The turns a minute of the record on the turntable.
NOTESscript.js2.2The notes a second rising from the record.
LIGHTSscript.js1How fast the light runs through the arch.
LIFTscript.js9How far a record rises when its title strip is pointed at.
BOUNCEscript.js1.6The bounce of the records when they drop into the rack.

Try them in the Playground

When to use it

For the menu of the site of a music bar, a diner, a venue with live music, a record shop or a radio: the items of the night (menu, drinks, gigs, bookings, where we are). From three to six items, with short titles like on real title strips.

On phones

The jukebox is as wide as the screen and at most as tall as the room left under the button; the mechanism is an SVG that fits itself and the title strips get shorter. Tapping a strip chooses it straight away: the record lifts only with a mouse or the keyboard.

With reduced motion

The jukebox fades in already open, with no running lights; choosing an item the record fades in already on the platter with the needle down, still, and the plate says the title. No moving claw, no spinning, no notes.

Pitfalls

  • The record in the claw must not be animated on its own: if its position and the claw’s have two different tweens, halfway the edge slips out of the fingers. Its position is worked out from the fingertips on every frame.
  • Laying the record down with scaleY, its centre has to come down by as much as the vertical radius shrinks, or the record comes away from the claw as it lands.
  • The light on the grooves must stay out of the group that turns: inside it would turn with the record and the record would look like a wheel.
  • A record put back in the rack must go back into the DOM in its place among the others, not at the end: otherwise it covers the neighbour that used to cover it.
  • The repeating stripe of light has to move by exactly one period along the horizontal (the period divided by the cosine of the angle), or the loop jumps.
  • The notes used to come out of the grille and, rising, passed over the title strips and covered the words: now they start from the record on the platter and rise in the dark behind the glass.

Libraries

GSAP 3.15.0, Bricolage Grotesque (font variabile) 5.3.0.

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

Testing

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