
The menu that opens from its button
Menus, Transitions, Text
Free
In the minimal style: the enquiry form of a design studio written as a single sentence, Hello, I’m … and I’d like … Every blank is a real field that widens as you write, and the words after it step aside on a spring of their own; a word pushed onto the next line waits for the line to make room and drops into it from above. Sent, the lines under the blanks close, what you wrote takes weight and a blue line signs the sentence.
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 form written as a sentence</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/full.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/full-italic.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/mona-sans@5.3.0/wdth.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the enquiry form of a design studio written as one sentence: every blank is a real field that grows with
what you write, and the words after it make room on a spring. Sending is pretended: see post() in the script -->
<form class="frase" action="#" novalidate aria-labelledby="frase-title" data-frase data-autoclic>
<h2 class="frase__eyebrow" id="frase-title">Start a project</h2>
<p class="frase__text" data-text>Hello, I’m <span class="tok" data-tok><span class="blank" data-blank="name"><label class="sr" for="frase-name">Your name</label><input class="blank__input" id="frase-name" name="name" type="text" autocomplete="name" autocapitalize="words" spellcheck="false" enterkeyhint="next" placeholder="your name" maxlength="40" aria-describedby="frase-name-err" aria-invalid="false" data-input><span class="blank__mirror" aria-hidden="true" data-mirror></span><span class="blank__caret" aria-hidden="true" data-caret></span><span class="blank__line" aria-hidden="true" data-line></span><span class="sr" id="frase-name-err" data-err></span></span>,</span> and I’d like <span class="tok" data-tok><span class="blank blank--choice" data-blank="kind"><label class="sr" for="frase-kind">What you need</label><select class="blank__select" id="frase-kind" name="kind" data-select>
<option>a new website</option>
<option>a brand identity</option>
<option>an online shop</option>
<option>a book design</option>
</select><span class="blank__face" aria-hidden="true" data-face><span class="face__word" data-word></span><svg class="face__chev" viewBox="0 0 12 12" aria-hidden="true"><path d="M2.5 4.5 6 8l3.5-3.5"/></svg></span><span class="blank__line" aria-hidden="true" data-line></span></span></span> for <span class="tok" data-tok><span class="blank" data-blank="work"><label class="sr" for="frase-work">What you do</label><input class="blank__input" id="frase-work" name="work" type="text" autocomplete="organization" spellcheck="true" enterkeyhint="next" placeholder="what you do" maxlength="48" aria-describedby="frase-work-err" aria-invalid="false" data-input><span class="blank__mirror" aria-hidden="true" data-mirror></span><span class="blank__caret" aria-hidden="true" data-caret></span><span class="blank__line" aria-hidden="true" data-line></span><span class="sr" id="frase-work-err" data-err></span></span>.</span> My budget is <span class="tok" data-tok><span class="blank blank--choice" data-blank="budget"><label class="sr" for="frase-budget">Your budget</label><select class="blank__select" id="frase-budget" name="budget" data-select>
<option>under €5k</option>
<option selected>€5–10k</option>
<option>€10–25k</option>
<option>over €25k</option>
</select><span class="blank__face" aria-hidden="true" data-face><span class="face__word" data-word></span><svg class="face__chev" viewBox="0 0 12 12" aria-hidden="true"><path d="M2.5 4.5 6 8l3.5-3.5"/></svg></span><span class="blank__line" aria-hidden="true" data-line></span></span>,</span> and you can write back to me at <span class="tok" data-tok><span class="blank" data-blank="email"><label class="sr" for="frase-email">Your email</label><input class="blank__input" id="frase-email" name="email" type="text" inputmode="email" autocomplete="email" autocapitalize="off" autocorrect="off" spellcheck="false" enterkeyhint="send" placeholder="your email" maxlength="80" aria-describedby="frase-email-err" aria-invalid="false" data-input><span class="blank__mirror" aria-hidden="true" data-mirror></span><span class="blank__caret" aria-hidden="true" data-caret></span><span class="blank__line" aria-hidden="true" data-line></span><span class="sr" id="frase-email-err" data-err></span></span>.</span></p>
<span class="frase__rule" aria-hidden="true" data-rule></span>
<div class="frase__foot">
<button class="send" type="submit" data-send>
<span class="send__bg" aria-hidden="true"></span>
<span class="send__label">Send enquiry</span>
</button>
<div class="frase__note" data-note><span class="note__line">We answer every enquiry within two days.</span></div>
</div>
<p class="sr" role="status" aria-live="polite" data-live></p>
</form>
</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.
41 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 form written as a sentence” from Animorio and fit it into my siteThe script splits the words of the sentence into inline-block spans (the tokens), with the spaces between them: the sentence wraps like ordinary text. Every blank is a real input (or a real select under the words it shows), with its label for screen readers, aria-invalid and aria-describedby to its own error, held in a token together with the comma or full stop after it, so the punctuation never wraps on its own.
Every change of size goes through relayout(): it reads offsetLeft and offsetTop of every token (the place without the transforms), changes the width of the blank, reads again, and the difference becomes the starting offset of one spring per token, integrated in small steps on the GSAP ticker (STIFF, DAMP, just under critical damping: one pixel too far). The springs add up, so typing fast never jumps. A token that ended up on another line waits invisible for 0.2 seconds instead, while the line makes room, and drops in from DROP pixels above.
The width of the blank is measured with a hidden twin in the same type. While you write in it it stays ROOM em longer than the text and grows by a whole ROOM when the writing reaches its end: so the words after it always move away from the letters and never over them. Left, it closes round the word. The choices change their words with a roll (the old one rises away, the new one rises from below) and their width follows a tween, which relayout turns step by step into the motion of the words after it.
On sending, every blank that is not right turns blue, its line stretches and comes back with elastic.out and its token gets a push on its spring (SHAKE). Beside the button the sentence goes on with the list of what is missing, each one a button that takes the focus to its blank; the list updates as you correct it and goes once everything is in place. A wrong email says what is missing: the @, the spaces, the ending like .com.
Once sent (post() pretends the request for WAIT seconds), the lines under the blanks close towards their middle in turn, the blanks tighten round the text, the weight of Fraunces rises from WRITTEN to SIGNED and the width of the arrows drops to zero, all in one tween that measures the blanks again inside relayout at every step: the sentence settles by itself. A blue line signs it from left to right; the button changes its word with a roll and takes the new width with a rebound.
| Name | File | Default | What it changes |
|---|---|---|---|
STIFF | script.js | 520 | How stiff the spring of every word is: higher, the words arrive sooner. |
DAMP | script.js | 34 | How much the spring is held back: lower, the words go further past before they settle. |
DROP | script.js | 14 | How far a word drops when it moves onto the next line, in pixels. |
ROOM | script.js | 1.2 | How far the blank stays ahead of the writing, in em (the width of an M). |
SHAKE | script.js | 90 | How hard a missing blank jolts on sending, in pixels per second. |
WRITTEN | script.js | 400 | The weight of what you write in the blanks. |
SIGNED | script.js | 600 | The weight what you wrote takes once the sentence is sent. |
WAIT | script.js | 0.8 | How many seconds the pretended sending takes. |
SENT | script.js | Thanks, {name}: we’ll write back within two days. | The line beside the button once sent; {name} is the name written. |
On the contact page of a studio, a photographer, a maker or a freelancer: wherever an enquiry has few questions and the tone matters as much as the answers. The sentence tells the writer what is needed without a list of boxes, and the signed sentence is the receipt of what was asked.
The sentence drops to 25 pixels and wraps onto six lines in the 358 pixels of a 390 phone, with the same springs; the button goes under the sentence and the line of messages under the button. A tap on a blank opens the right keyboard (email with the @, the Enter key saying Send in the last one), a tap on a choice opens the phone’s picker. The inputs are at least 16 pixels, so Safari does not zoom the page.
The words do not glide or drop: the sentence wraps at once, in place. The choices change their words with a fade, the errors turn blue without jolts; on sending lines and arrows fade, the text takes weight in one go and the blue line fades in.
GSAP 3.15.0, Fraunces (font variabile) 5.3.0, Mona Sans (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 11 KB; the libraries: GSAP 28 KB.
Tested on 19 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.