Vai al contenuto
animorio
EN
Tutti i componenti

Il menu del juke-box

Menu, Hover, SVGGSAP, Solo CSSImpegnativaPeso compresso: Demo 13 KBGSAP 28 KBGratisScarica lo zip

Nello stile ricco: il pulsante Menu di un bar musicale è la cima di un juke-box anni Cinquanta; premuto, sotto si apre il juke-box intero, il vetro sale e i dischi stanno in piedi nel caricatore. Ogni voce è un cartellino dei titoli: scegliendola, il braccio meccanico prende il suo disco, lo posa sul piatto, il disco gira, la puntina scende e dal disco salgono le note.

Il codice

Una cartella che funziona da sola: index.html, style.css, script.js e le librerie da CDN. Nelle schede React e Vue la stessa demo è già un componente, che spegne le sue animazioni quando esce dalla pagina.

<!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

Cambia valori, colori e testi con le manopole e guarda l’animazione rispondere dal vivo. Oppure scrivi tu nel codice. Poi scarica la cartella con le tue modifiche.

24 valori da cambiare

Oppure chiedila a Claude Code

Col connettore di Animorio, Claude Code la prende da qui con tutta la sua guida e la adatta al tuo progetto. Incolla questa richiesta:

Prendi «Il menu del juke-box» da Animorio e adattala al mio sito

Cosa fa

Su un fondo nero un pulsante a pillola rosso ciliegia col bordo cromato: dentro c’è la cima di un juke-box disegnata piccola, con l’arco di plastica, il vetro, un disco, i cartellini e la griglia. Passandoci sopra una luce scorre sull’arco e il dischetto fa un giro. Premendolo, la scritta diventa Close e sotto si apre il juke-box di Jive, un bar musicale: l’arco a tre bande di plastica, rossa, crema e menta, con strisce di luce che ci corrono dentro; il vetro con i riflessi che sale e sparisce nell’arco; dietro, su un fondo nero lucido scaldato da una luce dorata, cinque dischi che cadono in piedi nel caricatore cromato, ognuno con l’etichetta del suo colore, il giradischi col piatto di feltro rosso, il braccio della puntina appoggiato e, in alto, una guida cromata con un carrello e una pinza. Sotto ci sono i cartellini dei titoli, le voci vere del menu: Menu, Drinks, Live music, Book a table, Find us, ognuno con la fascia del colore del suo disco, e in fondo la griglia dell’altoparlante con la targhetta cromata. Puntando un cartellino il suo disco si solleva un poco. Scegliendolo, il carrello corre sopra il disco, la pinza scende, lo prende e lo solleva, lo porta sopra il piatto e mentre scende il disco si corica e si posa piatto; il disco comincia a girare con la luce che resta ferma sui solchi, il braccio si gira sul disco e la puntina si abbassa, la targhetta della griglia dice il titolo e dal disco salgono note che ondeggiano nel buio del vetro. Scegliendo un altro titolo, la puntina si alza, il disco rallenta, la pinza lo rimette al suo posto e va a prendere quello nuovo.

Come funziona

Un meccanismo fatto di pochi numeri

Tutto quello che si muove dentro il juke-box è un SVG di 400 per 200 che si ridisegna a ogni fotogramma da pochi numeri: dove sta il carrello sulla guida, quanto è lunga l’asta, quanto è chiusa la pinza, l’angolo del braccio della puntina, e per ogni disco la posizione, quanto è coricato e quanto ha girato. GSAP anima solo quei numeri; una funzione sola li trasforma in attributi transform.

Il disco preso dalla pinza

Mentre un disco è nella pinza la sua posizione non si anima: si calcola dalla pinza. Il centro sta sotto la punta delle dita di un raggio, moltiplicato per quanto il disco è coricato; così, mentre l’asta scende sul piatto e il disco passa da dritto (scala verticale 1) a coricato (0,3), il bordo resta sempre stretto fra le dita e il disco si posa sul feltro esattamente dove deve.

La luce che non gira

Ogni disco ha due gruppi: quello che gira (vinile, solchi, etichetta con la stampa e il foro) e sopra, fermi, due spicchi di luce opposti ritagliati sul cerchio. Quando il disco gira la stampa dell’etichetta corre e la luce resta lì, come su un disco vero. Coricato, tutto il disco è schiacciato in verticale e la rotazione diventa un giro su un’ellisse.

Le luci dell’arco e il vetro

L’arco sono tre bande di plastica una dentro l’altra con la stessa curva; in ognuna corre una striscia di luce, una sfumatura che si ripete spostata con background-position di esattamente un periodo, così il giro non ha scatti. Il vetro è un foglio di riflessi che all’apertura sale e sparisce dentro l’arco; il juke-box si srotola dall’alto con un clip-path e si assesta con un elastico.

Le note e i link veri

Le voci sono link veri, i cartellini dei titoli, raggiungibili col tasto Tab e con le frecce; il pulsante ha aria-expanded, il juke-box chiuso è inert, Esc chiude e ridà il fuoco al pulsante, e la targhetta della griglia dice il titolo in una regione aria-live. Mentre il disco suona, ogni tanto nasce una nota (una croma o due legate) disegnata in un SVG sopra il juke-box: parte dal disco sul piatto (il punto si converte dalle coordinate del meccanismo a quelle della pagina con getScreenCTM), sale ondeggiando nel buio del vetro, dove non copre nessuna scritta, si gira e si spegne.

Valori da cambiare

NomeFileDi partenzaCosa cambia
SPEEDscript.js1Quanto in fretta si muove il meccanismo (1 è normale).
RPMscript.js45I giri al minuto del disco sul piatto.
NOTESscript.js2.2Le note al secondo che salgono dal disco.
LIGHTSscript.js1Quanto in fretta corre la luce nell’arco.
LIFTscript.js9Quanto si solleva un disco quando si punta il suo cartellino.
BOUNCEscript.js1.6Il rimbalzo dei dischi quando cadono nel caricatore.

Provali nel Playground

Quando usarla

Per il menu del sito di un bar musicale, di un diner, di un locale con musica dal vivo, di un negozio di dischi o di una radio: le voci della serata (menu, bevande, concerti, prenotazioni, dove siamo). Da tre a sei voci, con titoli corti come sui cartellini veri.

Sul telefono

Il juke-box è largo quanto lo schermo e alto al massimo quanto resta sotto il pulsante; il meccanismo è un SVG che si adatta da solo e i cartellini si fanno più bassi. Toccare un cartellino lo sceglie subito: il disco si solleva solo col mouse o con la tastiera.

Con il movimento ridotto

Il juke-box compare in dissolvenza già aperto, senza luci che corrono; scegliendo una voce il disco compare in dissolvenza già sul piatto con la puntina giù, fermo, e la targhetta dice il titolo. Niente pinza in movimento, niente giri né note.

Trappole

  • Il disco nella pinza non va animato per conto suo: se la sua posizione e quella della pinza hanno due tween diversi, a metà strada il bordo esce dalle dita. La posizione si calcola dalla punta delle dita a ogni fotogramma.
  • Coricando il disco con scaleY il centro deve scendere di quanto si accorcia il raggio verticale, altrimenti il disco si stacca dalla pinza mentre si posa.
  • La luce sui solchi va tenuta fuori dal gruppo che gira: dentro girerebbe col disco e il disco sembrerebbe una ruota.
  • Un disco riportato nel caricatore va rimesso nel DOM al suo posto fra gli altri, non in fondo: se no copre il vicino che prima lo copriva.
  • La striscia di luce che si ripete va spostata di un periodo esatto lungo l’asse orizzontale (il periodo diviso il coseno dell’angolo), se no il giro fa uno scatto.
  • Le note uscivano dalla griglia e salendo passavano sopra i cartellini dei titoli, coprendo le parole: ora partono dal disco sul piatto e salgono nel buio del vetro.

Librerie

GSAP 3.15.0, Bricolage Grotesque (font variabile) 5.3.0.

Compressi con gzip, i tre file della demo pesano 13 KB; le librerie: GSAP 28 KB.

Collaudo

Provata il 22 ottobre 2026

  • Computer: Fatto
  • Telefono: Fatto
  • Movimento ridotto: Fatto
  • Safari su iPhone: Da fare

Licenza d’uso

Puoi usarla nei tuoi progetti e in quelli dei tuoi clienti. Non puoi rivenderla né ridistribuirla così com’è. GSAP resta sotto la sua licenza Standard. Licenza completa.

Tutti i componenti