
Il menu dalle tre righe
Menu, Transizioni, Fisica, Testo
NuovaGratis
La ricerca di un sito di cucina dove le parole scritte stanno in mezzo al campo e ogni ricetta suggerita scivola di lato finché le stesse lettere non stanno proprio sotto: le lettere trovate fanno una colonna, segnata da un tratto di evidenziatore che scende dal campo lungo la lista. Le ricette nuove si srotolano dalle loro lettere, quelle che non valgono più ci si riarrotolano dentro, e quella presa sale nel campo posandosi sulle lettere scritte.
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 column search</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/alegreya@5.3.0/wght.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/alegreya@5.3.0/wght-italic.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the search of a cooking site. The words typed sit in the middle of the field, and every recipe suggested
slides sideways until the same letters sit right under them: the letters found make a column, marked down
the list like a highlighter. A new recipe unrolls from its letters, a chosen one rises into the field.
The rows are made by the script from the list at the top of script.js -->
<div class="seek" data-seek data-autoclic>
<form class="seek__form" role="search" action="#" novalidate data-form>
<div class="seek__field" data-field>
<span class="seek__swipe" aria-hidden="true" data-swipe></span>
<svg class="seek__lens" viewBox="0 0 24 24" aria-hidden="true"><path d="M10.5 4.5a6 6 0 1 0 0 12a6 6 0 1 0 0-12zM15 15l4.6 4.6"/></svg>
<input class="seek__input" id="seek-input" type="text" inputmode="search" enterkeyhint="search" role="combobox" aria-label="Search recipes" aria-autocomplete="list" aria-expanded="false" aria-controls="seek-list" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" placeholder="Search recipes" data-input>
<span class="seek__caret" aria-hidden="true" data-caret></span>
<button class="seek__clear" type="button" aria-label="Clear the search" tabindex="-1" data-clear>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7.5 7.5l9 9M16.5 7.5l-9 9"/></svg>
</button>
<button class="seek__go" type="submit" aria-label="Search" aria-disabled="true" data-go>
<span class="seek__arrow" aria-hidden="true" data-arrow><svg viewBox="0 0 24 24"><path d="M5 12h13M12.5 6.5 18 12l-5.5 5.5"/></svg></span>
</button>
</div>
<div class="seek__panel" data-panel>
<div class="seek__rows" data-rows>
<span class="seek__band" aria-hidden="true" data-band></span>
<span class="seek__beam" aria-hidden="true" data-beam></span>
<ul class="seek__list" id="seek-list" role="listbox" aria-label="Recipes" data-list></ul>
</div>
<div class="seek__none" data-none hidden>
<p class="none__what" data-none-what></p>
<p class="none__tip" data-none-tip></p>
</div>
</div>
<p class="seek__note" data-note><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M4 10h11M10.5 5.5 15 10l-4.5 4.5"/></svg><span data-note-words></span></p>
<p class="sr" role="status" aria-live="polite" data-said></p>
</form>
<div class="seek__flight" aria-hidden="true" data-flight></div>
<!-- the highlighter's edge: not ruled, but a little uneven, like a real marker's -->
<svg class="seek__defs" aria-hidden="true" focusable="false">
<filter id="seek-marker" x="-10%" y="-4%" width="120%" height="108%">
<feTurbulence type="fractalNoise" baseFrequency="0.03 0.07" numOctaves="2" seed="7" result="grain"/>
<feDisplacementMap in="SourceGraphic" in2="grain" scale="2.4" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</svg>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="script.js"></script>
</body>
</html>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.
60 valori da cambiare
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 «La ricerca in colonna» da Animorio e adattala al mio sitoL’input è vero: role combobox, aria-expanded, aria-controls verso la lista (role listbox) e aria-activedescendant sulla ricetta scelta, che ha aria-selected. Ha text-align center e lo stesso spazio ai due lati, così le parole scritte stanno nel mezzo; la crocetta per svuotarlo prende il posto della lente, a sinistra, per non rubare spazio. La larghezza delle parole si misura con una copia nascosta nello stesso carattere dell’input (getBoundingClientRect, coi decimali); se sono più larghe del campo partono dal bordo e si toglie lo scorrimento (scrollLeft).
search() confronta le lettere senza maiuscole né accenti: ogni lettera diventa una sola lettera (é diventa e), così un punto nel nome semplice è lo stesso punto nel nome vero. Per ogni ricetta tiene il punto migliore, l’inizio del nome, poi l’inizio di una parola, poi dentro una parola, e a parità l’ordine di RECIPES; ne mostra SHOWN e le lettere trovate vanno in un mark. Quando nulla corrisponde, ogni parola scritta che non sta in nessun nome si confronta con le parole delle ricette con la distanza di Levenshtein (una lettera per le parole corte, due da sei lettere in su).
Per ogni riga si misura dove sta il mark dentro il nome (il suo getBoundingClientRect meno quello del nome) e la riga si sposta di lato del centro delle parole scritte meno il centro del mark: le lettere trovate cadono esattamente sotto quelle del campo, al centesimo di pixel. L’evidenziatore è un solo elemento, largo quanto il mark più largo, con un filtro SVG (feTurbulence e feDisplacementMap) che ne rende il bordo irregolare come quello di un pennarello e mix-blend-mode multiply, così sopra la fascia grigia si scurisce come l’inchiostro vero.
La posizione di ogni riga, di lato e in altezza, sta su due molle (SPRING: rigidità e smorzamento) integrate a passi di 1/240 di secondo nel ticker di GSAP, che smette di ridisegnare quando tutto è fermo; scrivendo in fretta le molle cambiano solo bersaglio, senza ripartire. Fra la lista di prima e quella nuova si cerca la sottosequenza crescente più lunga delle righe che restano: quelle scivolano nel loro ordine e non si incrociano mai, le altre se ne vanno e rientrano al posto nuovo. Chi resta aspetta 0,08 secondi che chi va sia svanito, e le righe nuove arrivano quando le altre sono quasi ferme.
Una riga nuova compare con un clip-path inset() che all’inizio lascia vedere solo il mark e si apre fino al nome intero in UNROLL secondi (power3.out): sembra uscire dalle sue lettere, verso sinistra e verso destra. Chi se ne va fa il contrario in 0,16 secondi. All’apertura le righe arrivano una dopo l’altra mentre il tratto giallo si disegna dall’alto (scaleY); quando nulla corrisponde risale nel campo.
Il nome scelto si copia in un livello sopra tutto, nel punto esatto in cui sta, e sale in metà di LIFT secondi: le sue lettere trovate sono già sotto quelle scritte, quindi ci si posano sopra senza spostarsi di lato. Poi scivola nel mezzo del campo, il mark passa da 700 a 420 di peso (il carattere è variabile) e l’evidenziatore, rimisurato a ogni fotogramma, lo segue allargandosi dalle lettere trovate a tutto il nome; all’arrivo l’input prende il nome e la copia sparisce. Un clic da script (detail 0, l’anteprima della libreria) scrive senza prendere il fuoco, con un cursore disegnato, un passo per clic: ri, poi s e le frecce, la ricetta presa, un refuso e la parola vicina, pe e la ricerca.
| Nome | File | Di partenza | Cosa cambia |
|---|---|---|---|
SHOWN | script.js | 6 | Quante ricette mostra al massimo la lista. |
ROW | script.js | 48 | L’altezza di una riga, in pixel. |
SPRING | script.js | [420, 30] | La molla che tiene le righe sulla colonna: rigidità e smorzamento. |
UNROLL | script.js | 0.45 | I secondi in cui una ricetta nuova si srotola dalle sue lettere. |
LIFT | script.js | 0.7 | I secondi in cui la ricetta presa sale nel campo e si sistema nel mezzo. |
TRIES | script.js | ['lemon', 'saffron', 'pear'] | Gli ingredienti proposti quando nulla corrisponde e nessuna parola è vicina. |
RECIPES | script.js | ['Saffron risotto', 'Lemon ricotta pancakes', …] | Le ricette in cui cercare, dalle più cucinate. |
Per la ricerca di un sito di ricette, di un negozio, di una libreria, di una documentazione: ovunque i nomi da trovare sono brevi e serve vedere subito perché un risultato è stato trovato, anche quando le lettere cercate stanno in mezzo al nome. La funzione search() è la sola da cambiare per chiedere i risultati a un server.
Il campo prende la larghezza dello schermo meno 32 pixel e resta alto 60; le parole sono di 17 pixel (da 16 in su iPhone non ingrandisce la pagina toccando il campo) e la tastiera mostra il tasto per cercare. Le righe sono alte 48 pixel per il dito e si prendono toccandole, senza che il campo perda il fuoco; con la crocetta al posto della lente restano 240 pixel per scrivere, e i nomi della lista ci stanno interi anche con la colonna nel mezzo.
Niente molle, srotolamenti né voli: le righe compaiono e spariscono in dissolvenza già al loro posto sulla colonna, il tratto giallo compare in dissolvenza invece di scendere, la fascia salta da una riga all’altra e la lente diventa crocetta in dissolvenza. La ricetta presa svanisce dalla lista e il nome compare nel campo; la freccia non esce dal quadrato, si spegne e si riaccende. La colonna, i consigli e la tastiera funzionano uguali.
GSAP 3.15.0, Alegreya (font variabile) 5.3.0.
Compressi con gzip, i tre file della demo pesano 15 KB; le librerie: GSAP 28 KB.
Provata il 6 ottobre 2026
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.
Con l’email e la password del tuo account.
oppure con l’email
Hai fatto l’accesso come .
Con l’account scarichi lo zip di ogni animazione e di ogni pacchetto, gratis.