Vai al contenuto
animorio
EN
Tutti i componenti

La chat che scrive la risposta

Chat, Testo, TransizioniGSAP, Solo CSSImpegnativaPeso compresso: Demo 12 KBGSAP 28 KBGratisScarica lo zip

L’assistente di cucina di un’app di ricette, nello stile minimal: la risposta arriva parola per parola, come in streaming, e ogni parola entra stretta e leggera sugli assi del carattere e si posa alla larghezza e al peso normali. Un cursore vermiglio sottile corre alla fine del testo; prima della prima parola l’attesa è una linea sottile che si allunga e poi si alza nel cursore. Stop lascia la frase a metà, Regenerate riavvolge le parole dall’ultima e ne scrive un’altra. Le righe le va a capo lo script con la larghezza finale di ogni parola, così niente salta a capo mentre si allarga.

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 chat that writes its answer</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/roboto-flex@5.3.0/wdth.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the kitchen assistant of a cooking app. Its answer comes in word by word, as a stream: every word arrives
         narrow and light and settles to its normal width and weight, and a thin vermilion caret rides the end of the
         text. Before the first word the wait is a hairline that lengthens; Stop leaves the sentence where it was,
         Regenerate winds the answer back from its last word and writes another one -->
    <section class="ask" aria-label="Mise, kitchen assistant" data-panel data-autoclic>
      <header class="ask__head">
        <span class="ask__brand">Mise</span>
        <span class="ask__role">Kitchen assistant</span>
        <span class="ask__rule" aria-hidden="true" data-rule></span>
      </header>

      <div class="ask__thread" data-thread>
        <p class="ask__q" data-question>I have two leeks, four eggs and a little parmesan. What can I make tonight?</p>

        <div class="ask__a" aria-busy="false" data-answer>
          <p class="ask__text" aria-hidden="true" data-text></p>
          <span class="ask__caret" aria-hidden="true" data-caret></span>
          <p class="sr" aria-live="polite" data-live></p>

          <div class="ask__after" data-after>
            <button class="regen" type="button" data-regen>
              <svg class="regen__icon" viewBox="0 0 20 20" aria-hidden="true" data-regen-icon>
                <path d="M15.6 7.4A6.2 6.2 0 1 0 16.2 12"/>
                <path d="M16.1 3.6v4.1H12"/>
              </svg>
              <span class="regen__label">Regenerate</span>
            </button>
            <span class="ask__note" data-note>Stopped</span>
          </div>
        </div>
      </div>

      <form class="ask__composer" data-form>
        <div class="field">
          <label class="sr" for="mise-input">Ask Mise a follow-up</label>
          <input class="field__input" id="mise-input" type="text" placeholder="Ask a follow-up" autocomplete="off" maxlength="160" enterkeyhint="send" data-input>
          <button class="act" type="button" aria-label="Send" disabled data-act>
            <span class="act__icons" aria-hidden="true">
              <svg class="act__arrow" viewBox="0 0 24 24" data-arrow><path d="M12 18.4V5.8M6.8 11 12 5.8l5.2 5.2"/></svg>
              <svg class="act__square" viewBox="0 0 24 24" data-square><rect x="7.6" y="7.6" width="8.8" height="8.8" rx="1.6"/></svg>
            </span>
            <span class="act__label" aria-hidden="true" data-act-label>Stop</span>
          </button>
        </div>
      </form>
    </section>
  </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.

26 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 chat che scrive la risposta» da Animorio e adattala al mio sito

Cosa fa

Su un fondo bianco un riquadro bianco alto 560 pixel e largo 384, col bordo sottile e gli angoli tondi. In alto Mise, stretto e nero, e Kitchen assistant in grigio, sopra un filetto. Sotto, la domanda in grande: I have two leeks, four eggs and a little parmesan. What can I make tonight? Poi la risposta, una frittata di porri, con Regenerate sotto, e in fondo il campo Ask a follow-up col pulsante tondo della freccia. Premendo Regenerate le parole si riavvolgono dall’ultima alla prima, si stringono, perdono peso e spariscono come un’onda all’indietro; al loro posto una linea vermiglia sottile si allunga da sinistra: l’attesa. Quando arriva la risposta la linea torna indietro e si alza in un cursore sottile, e le parole entrano a gruppetti irregolari come in uno streaming vero: ognuna arriva strettissima, leggera e trasparente, cade di un capello e si allarga fino alla sua larghezza, spingendo avanti le altre e il cursore. A ogni riga nuova il cursore sparisce dov’è e rispunta alla fine della riga nuova, senza passare sopra al testo. Intanto il pulsante del campo diventa una pillola nera con un quadratino e Stop. Premuto Stop, la frase resta a metà, il cursore diventa grigio e sotto compaiono Regenerate e Stopped. Finita la risposta il cursore si abbassa e sparisce. Scrivendo una domanda nel campo, la risposta vecchia si riavvolge, la domanda sale al suo posto e la risposta nuova si scrive allo stesso modo (prova Can I do it without eggs?, Which wine goes with it? o How long does it take?).

Come funziona

Ogni parola è un blocco con gli assi in due variabili

Ogni parola della risposta è uno span inline-block con font-variation-settings: 'wdth' var(--wd), 'wght' var(--wg) in Roboto Flex (larghezza da 25 a 151, peso da 100 a 1000). GSAP anima le due variabili CSS di ogni parola da ARRIVE a REST con power3.out e power2.out, l’opacità in un attimo e una caduta di 3 pixel con back.out: la parola entra stretta e leggera e si posa. Le parole sono blocchi perché il transform su un elemento inline non funziona.

Gli a capo li fa lo script, con le larghezze finali

Una parola che si allarga dopo essere entrata spingerebbe l’ultima della riga a capo, a metà movimento. Per questo appena arriva una parola si misura la sua larghezza da posata su un righello nascosto con gli stessi assi (una volta sola per parola), e lo script tiene il conto di quanto è lunga la riga a parole posate: se la parola nuova non ci starebbe, prima di lei mette un br. Mentre le parole sono strette la riga è più corta della misura, quindi il browser non va mai a capo da solo, e quello che è già scritto non si muove. A ogni cambio di misura gli a capo si rifanno con le larghezze nuove.

Il ritmo dello streaming

I tempi delle parole si calcolano prima, con un numero a caso che nasce dal testo (la stessa risposta ha sempre lo stesso ritmo): in media SPEED parole al secondo, a volte due nello stesso pezzo, un respiro dopo le virgole e uno più lungo dopo i punti. Ogni parola è una call su una timeline GSAP: Stop uccide la timeline e le parole già partite finiscono di posarsi.

Un pixel solo, linea e cursore

La linea dell’attesa e il cursore sono lo stesso elemento di 1 pixel per 1, allungato col transform a ogni fotogramma. Un valore m va da 0 (linea sdraiata dall’inizio della riga, lunga quanto vuole l’attesa) a 1 (cursore in piedi alla fine del testo): nella prima metà la punta della linea torna indietro, nella seconda la linea si alza con un back.out, un pixel di troppo e il ritorno. Il cursore segue la fine dell’ultima parola, misurata a ogni fotogramma, su una molla; sulla sua riga non resta mai indietro (le parole lo spingono), e quando la riga cambia scende a zero dov’è e rinasce alla fine della riga nuova, così non attraversa mai il testo.

Stop, Regenerate e la domanda nuova

Il pulsante del campo diventa Stop allargandosi con un back.out, la freccia esce in alto e il quadratino entra. Stop lascia la frase com’è, grigia la punta del cursore, e mostra Regenerate e Stopped. Regenerate passa alla risposta dopo in ANSWERS: le parole tornano ai valori d’arrivo dall’ultima alla prima, sfalsate, e la linea dell’attesa ricomincia. Una domanda scritta nel campo cerca una parola di FOLLOW_UPS nella domanda (altrimenti risponde OTHER).

Tasti, lettori di schermo e anteprima

Le parole che arrivano sono aria-hidden: la risposta intera va in una regione aria-live solo quando finisce o si ferma, e intanto il riquadro della risposta è aria-busy. Esc ferma la risposta; se Regenerate aveva il fuoco quando sparisce, il fuoco passa a Stop, e a Regenerate quando la risposta finisce. Un clic da script sul riquadro (l’anteprima della libreria) è un giro di tutti gli stati: Regenerate, l’attesa, l’altra risposta fermata con Stop a metà di una frase, una pausa, di nuovo Regenerate e la prima risposta scritta fino in fondo, cioè com’era all’inizio.

Valori da cambiare

NomeFileDi partenzaCosa cambia
SPEEDscript.js13Le parole al secondo dello streaming, in media (arrivano a pezzi irregolari, come in uno vero).
SETTLEscript.js0.55I secondi in cui una parola passa da stretta e leggera alla sua forma normale.
ARRIVEscript.js[40, 190]La larghezza e il peso con cui arriva una parola (larghezza da 25 a 151, peso da 100 a 1000).
RESTscript.js[100, 400]La larghezza e il peso a cui si posa.
WAITscript.js0.9I secondi della linea sottile che si allunga prima della prima parola.
QUESTIONscript.js'I have two leeks, four eggs and a little parmesan. What can I make tonight?'La domanda fatta all’assistente.
ANSWERSscript.js['A leek frittata. …', 'Baked eggs on soft leeks. …']Le risposte alla domanda: Regenerate scrive quella dopo.
FOLLOW_UPSscript.js[['egg', 'Then make it a leek risotto. …'], …]Una parola da cercare in una domanda scritta nel campo, e la risposta.
OTHERscript.js'I can help with what is on your counter: …'La risposta a tutto il resto.

Provali nel Playground

Quando usarla

Per l’assistente di un’app o di un sito: le ricette, i viaggi, l’assistenza clienti, la documentazione di un prodotto, un aiuto nelle impostazioni. Sta bene dove una risposta lunga deve arrivare man mano senza far ballare la pagina. Colori nelle variabili, domanda e risposte nel blocco dei valori; con un modello vero basta chiamare la funzione che aggiunge una parola a ogni pezzo che arriva.

Sul telefono

Il riquadro prende la larghezza dello schermo meno 32 pixel (358 a 390), la domanda scende a 18 pixel e le righe si rifanno con la misura nuova; le parole entrano e si posano allo stesso modo. Il campo è a 16 pixel, così Safari su iPhone non ingrandisce la pagina, e la tastiera mostra il tasto per inviare. Stop e Regenerate sono alti almeno 34 pixel; sotto il dito Regenerate si allarga e prende peso come sotto il puntatore.

Con il movimento ridotto

Le parole arrivano ancora una per una, ma già alla loro larghezza e al loro peso, con una breve dissolvenza; la linea dell’attesa compare ferma, il cursore salta alla fine del testo senza molla, Regenerate e una domanda nuova fanno sparire la risposta in dissolvenza e il pulsante cambia senza allargarsi.

Trappole

  • Con le parole che si allargano nel testo normale, l’ultima della riga entrava stretta, ci stava, e si allargava fino a saltare a capo a metà movimento. Gli a capo ora li mette lo script con le larghezze da posate, misurate su un righello nascosto.
  • Il righello leggeva lo spazio fra le parole al primo ResizeObserver, quando Roboto Flex non era ancora arrivato: lo spazio del carattere di riserva cambiava gli a capo, e alla fine del giro la risposta non tornava uguale. Ora si misura solo dopo document.fonts.load.
  • La linea dell’attesa era ancora sdraiata quando arrivava la prima parola e la tagliava a metà come un barrato: ora la linea torna indietro e si alza, e la prima parola parte un terzo di secondo dopo.
  • Nel passaggio da linea a cursore larghezza e altezza crescevano insieme, e a metà strada c’era un rettangolo rosso pieno: ora prima si accorcia, poi si alza.
  • Con una molla anche in verticale il cursore, andando a capo, passava in diagonale sopra le ultime parole della riga: ora sparisce dov’è e rinasce alla fine della riga nuova; sulla sua riga non può restare dietro al testo, quindi non taglia mai una parola che si allarga.
  • Una tween creata dentro una call di GSAP comincia solo al fotogramma dopo: il ciclo del cursore la vedeva ferma, si spegneva e il cursore restava acceso dopo la fine. Ora il ciclo si ferma dopo qualche fotogramma tranquillo.
  • Il giro dell’anteprima fermava la risposta dopo il 48% delle parole, proprio su un punto: ora Stop cade sulla prima parola dopo un terzo della risposta che non ha punteggiatura né lei né le due dopo, a metà di una frase.

Librerie

GSAP 3.15.0, Roboto Flex (font variabile, con gli assi della larghezza e del peso) 5.3.0.

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

Collaudo

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