Vai al contenuto
animorio
EN
Tutti i componenti

La ricerca in colonna

Ricerca, Testo, Transizioni, FisicaGSAP, Solo CSSImpegnativaPeso compresso: Demo 15 KBGSAP 28 KBGratisScarica lo zip

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.

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

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.

60 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 «La ricerca in colonna» da Animorio e adattala al mio sito

Cosa fa

Su un fondo bianco un campo bianco dagli angoli tondi, con una lente a sinistra, la scritta in corsivo Search recipes nel mezzo e a destra un quadrato scuro con una freccia, spento finché il campo è vuoto. Scrivendo, le lettere stanno al centro del campo su una passata di evidenziatore giallo, la lente gira su se stessa e diventa una crocetta per svuotare il campo, e il quadrato si accende. Alla prima lettera sotto il campo si apre la lista e un tratto di evidenziatore scende dall’alto proprio sotto le lettere scritte, coi bordi un po’ irregolari come quelli di un pennarello vero; lungo il tratto compaiono le ricette una dopo l’altra, prima solo le lettere che combaciano, in grassetto sul giallo, poi il resto del nome che si srotola a sinistra e a destra. Ogni ricetta è spostata di lato quanto basta perché le sue lettere stiano esattamente sotto quelle del campo, così si legge una colonna: Saffron risotto, Lemon ricotta pancakes e Apricot rice pudding hanno il ri uno sotto l’altro. Vengono prima i nomi che cominciano così, poi quelli con una parola che comincia così, poi quelli con le lettere dentro una parola. A ogni lettera in più le ricette che non valgono più si riarrotolano nelle loro lettere e svaniscono, quelle che restano aspettano un attimo e scivolano su una molla al posto nuovo e di lato per restare sulla colonna, e le nuove si srotolano dove c’è posto; il tratto giallo si allarga o si stringe con le lettere e la lista si allunga o si accorcia. Le frecce o il puntatore mettono una fascia grigia sotto una ricetta, che diventa nera; Invio, un clic o un tocco la prendono: le altre svaniscono, la lista risale nel campo e il nome sale dritto finché le sue lettere si posano su quelle scritte, poi scivola nel mezzo del campo mentre l’evidenziatore si allarga sotto tutto il nome e sfuma, e sotto il campo si legge Opening col nome. Invio senza una ricetta scelta, o il quadrato con la freccia, cerca: la freccia esce a destra e rientra da sinistra, la lista si chiude e sotto si legge quante ricette ci sono, per esempio 5 recipes for “pe”. Se nulla corrisponde l’evidenziatore nel campo diventa grigio, il tratto giallo risale e la lista dice No recipes for con le lettere scritte, e un consiglio: Did you mean risotto? se una parola delle ricette è vicina a quella scritta, altrimenti Try an ingredient, like lemon, saffron or pear. La parola proposta, toccata, sale nel campo allo stesso modo e la ricerca riparte con lei. Esc chiude la lista e, a lista chiusa, svuota il campo.

Come funziona

Le parole nel mezzo del campo

L’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).

La ricerca

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).

La colonna

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.

Righe su molle che non si incrociano

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.

Srotolarsi dalle lettere

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.

Prendere una ricetta

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.

Valori da cambiare

NomeFileDi partenzaCosa cambia
SHOWNscript.js6Quante ricette mostra al massimo la lista.
ROWscript.js48L’altezza di una riga, in pixel.
SPRINGscript.js[420, 30]La molla che tiene le righe sulla colonna: rigidità e smorzamento.
UNROLLscript.js0.45I secondi in cui una ricetta nuova si srotola dalle sue lettere.
LIFTscript.js0.7I secondi in cui la ricetta presa sale nel campo e si sistema nel mezzo.
TRIESscript.js['lemon', 'saffron', 'pear']Gli ingredienti proposti quando nulla corrisponde e nessuna parola è vicina.
RECIPESscript.js['Saffron risotto', 'Lemon ricotta pancakes', …]Le ricette in cui cercare, dalle più cucinate.

Provali nel Playground

Quando usarla

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.

Sul telefono

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.

Con il movimento ridotto

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.

Trappole

  • Le righe stanno tutte in posizione assoluta, quindi la lista era alta zero e la maschera che ne sfuma i bordi (mask-image) la tagliava tutta: non si vedeva nessuna ricetta. La lista ora riempie il pannello (position absolute, inset 0).
  • Le righe che restavano scivolavano subito al posto nuovo e passavano sopra quelle che se ne andavano, con le parole sovrapposte: ora chi se ne va svanisce in 0,12 secondi, chi resta aspetta 0,08 secondi prima di muoversi e le righe nuove arrivano quando le altre sono quasi ferme.
  • Una chiusura della lista partita con un piccolo ritardo non viene fermata da overwrite: 'auto' di un tween nuovo, perché non è ancora cominciata: svuotando il campo e scrivendo subito, la lista si apriva e un attimo dopo si richiudeva vuota. Ora i tween dell’altezza della lista si fermano con killTweensOf prima di ogni nuovo.
  • Il nome che sale verso il campo spariva a metà strada: passava sotto la lista, che ha uno z-index per coprire la riga dei messaggi. Il livello del volo ora ne ha uno più alto.
  • La riga sotto il campo (5 recipes for “pe”) compariva mentre la lista si stava ancora chiudendo, con le sue parole sopra quelle delle ricette: ora aspetta che la lista sia risalita.
  • Mentre il nome scivolava nel mezzo del campo l’evidenziatore restava dov’era e per un attimo segnava le lettere sbagliate: ora a ogni fotogramma si misura il mark che vola e l’evidenziatore lo segue.
  • Per tenere le parole nel mezzo il campo deve lasciare lo stesso spazio ai due lati: con la crocetta a destra accanto alla freccia, su un telefono restavano 180 pixel per scrivere e Fennel and orange salad non ci stava. La crocetta ora prende il posto della lente e lo spazio è di 240 pixel.
  • Su un telefono un tocco su una riga può togliere il fuoco al campo prima del clic: la lista, chiusa subito, si portava via anche la riga toccata. La chiusura per il fuoco perso ora aspetta 0,2 secondi e non avviene se intanto il nome ha preso il volo.

Librerie

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.

Collaudo

Provata il 6 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