
The menu that opens from its button
Menus, Transitions, Text
Free
The kitchen assistant of a recipe app, in the minimal style: the answer comes in word by word, as a stream, and every word arrives narrow and light on the font’s axes and settles to its normal width and weight. A thin vermilion caret rides the end of the text; before the first word the wait is a hairline that lengthens and then stands up into the caret. Stop leaves the sentence halfway, Regenerate winds the words back from the last one and writes another answer. The script breaks the lines with each word’s final width, so nothing jumps to the next line while it widens.
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 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>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.
26 values to change
With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:
Take “The chat that writes its answer” from Animorio and fit it into my siteEvery word of the answer is an inline-block span with font-variation-settings: 'wdth' var(--wd), 'wght' var(--wg) in Roboto Flex (width 25 to 151, weight 100 to 1000). GSAP tweens each word’s two CSS variables from ARRIVE to REST with power3.out and power2.out, the opacity in a moment and a 3 pixel drop with back.out: the word comes in narrow and light and settles. The words are blocks because a transform does nothing on an inline element.
A word that widens after it has come in would push the last one on the line onto the next, halfway through its movement. So the moment a word arrives its settled width is measured on a hidden ruler with the same axes (once per word), and the script keeps count of how long the line is with the words settled: if the new word would not fit, a br goes in before it. While the words are narrow the line is shorter than the measure, so the browser never breaks a line by itself, and what is already written never moves. Whenever the measure changes the line breaks are made again with the new widths.
The words’ times are worked out beforehand, with a random number seeded by the text (the same answer always has the same rhythm): SPEED words a second on average, sometimes two in one chunk, a breath after commas and a longer one after full stops. Every word is a call on a GSAP timeline: Stop kills the timeline and the words already on their way finish settling.
The waiting line and the caret are the same element, 1 pixel by 1, stretched by its transform every frame. A value m goes from 0 (the line lying from the start of the row, as long as the wait wants) to 1 (the caret standing at the end of the text): in the first half the line’s tip runs back, in the second the line stands up with a back.out, a pixel too far and back. The caret follows the end of the last word, measured every frame, on a spring; on its own line it never falls behind (the words push it), and when the line changes it shrinks to nothing where it is and is born again at the end of the new line, so it never crosses the text.
The field’s button turns into Stop widening with a back.out, the arrow leaves upwards and the square comes in. Stop leaves the sentence as it is, turns the caret grey and shows Regenerate and Stopped. Regenerate moves to the next answer in ANSWERS: the words go back to their arrival values from the last to the first, staggered, and the waiting line starts again. A question typed into the field looks for a word of FOLLOW_UPS in it (otherwise the answer is OTHER).
The words coming in are aria-hidden: the whole answer goes into an aria-live region only when it finishes or stops, and meanwhile the answer’s box is aria-busy. Esc stops the answer; if Regenerate had the focus when it goes, the focus moves to Stop, and back to Regenerate when the answer ends. A click from a script on the box (the library’s preview) is a tour of every state: Regenerate, the wait, the other answer stopped halfway through a sentence, a pause, Regenerate again and the first answer written to the end, which is how it started.
| Name | File | Default | What it changes |
|---|---|---|---|
SPEED | script.js | 13 | The words per second of the stream, on average (they come in uneven chunks, like a real one). |
SETTLE | script.js | 0.55 | The seconds a word takes to go from narrow and light to its normal shape. |
ARRIVE | script.js | [40, 190] | The width and the weight a word arrives at (width 25 to 151, weight 100 to 1000). |
REST | script.js | [100, 400] | The width and the weight it settles at. |
WAIT | script.js | 0.9 | The seconds of the thin line that lengthens before the first word. |
QUESTION | script.js | 'I have two leeks, four eggs and a little parmesan. What can I make tonight?' | The question asked to the assistant. |
ANSWERS | script.js | ['A leek frittata. …', 'Baked eggs on soft leeks. …'] | The answers to the question: Regenerate writes the next one. |
FOLLOW_UPS | script.js | [['egg', 'Then make it a leek risotto. …'], …] | A word to look for in a question typed into the field, and the answer to it. |
OTHER | script.js | 'I can help with what is on your counter: …' | The answer to anything else. |
For the assistant of an app or a website: recipes, travel, customer help, a product’s documentation, help in the settings. It suits any place where a long answer has to arrive bit by bit without making the page jump. Colours in the variables, question and answers in the values block; with a real model, call the function that adds a word for every chunk that comes in.
The box takes the width of the screen less 32 pixels (358 at 390), the question goes down to 18 pixels and the lines are broken again for the new measure; the words come in and settle the same way. The field is 16 pixels, so Safari on iPhone does not zoom the page, and the keyboard shows its send key. Stop and Regenerate are at least 34 pixels tall; under a finger Regenerate widens and takes weight as under the pointer.
The words still arrive one by one, but already at their width and weight, with a short fade; the waiting line appears still, the caret jumps to the end of the text with no spring, Regenerate and a new question fade the answer out and the button changes without widening.
GSAP 3.15.0, Roboto Flex (font variabile, con gli assi della larghezza e del peso) 5.3.0.
Compressed with gzip, the three demo files weigh 12 KB; the libraries: GSAP 28 KB.
Tested on 16 October 2026
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.
With your account’s email and password.
or with your email
You’re signed in as .
With your account, you can download the zip of every animation and every pack, for free.