Skip to content
animorio
IT
All components

The form written as a sentence

Forms, Text, TransitionsGSAP, CSS onlyAdvancedCompressed weight: Demo 11 KBGSAP 28 KBFreeDownload the zip

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.

The code

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>

Playground

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

Or ask Claude Code for it

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 site

What it does

On a white page, under the small words Start a project, a large serif sentence: Hello, I’m your name, and I’d like a new website for what you do. My budget is €5–10k, and you can write back to me at your email. The grey italic words are the blanks to fill, each with a thin line under it; a new website and €5–10k are two choices with a small arrow. Below sits the black Send enquiry pill and beside it We answer every enquiry within two days. Writing in a blank, its line turns black, the blank stays a little longer than what you wrote and when the writing reaches its end it grows in one go: the words after it glide right on a spring and settle a pixel too far; the one that no longer fits vanishes from the end of the line, the line below opens a space for it and it drops into it from above. Leaving the blank it closes round the word and the others come back. Changing a choice the old word rises away, the new one rises from below and the blank opens or closes with the words following it. Pressing Send enquiry with something missing, that blank turns blue, gives a small jolt on its spring and beside the button the sentence goes on: Almost there. Add an email to write back to., with the name of every missing blank in blue to tap to go back to it. With everything in place the lines under the blanks close towards their middle one after the other, the arrows fade, the written words grow heavier and the sentence settles again; a blue line underlines it from left to right and the button becomes Sent, with Thanks, Mira: we’ll write back within two days. beside it and Edit to write again.

How it works

A sentence made of tokens

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

FLIP with a spring for every word

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 blank that stays ahead of the writing

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.

The errors as the end of the sentence

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.

The signed sentence

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.

Values to change

NameFileDefaultWhat it changes
STIFFscript.js520How stiff the spring of every word is: higher, the words arrive sooner.
DAMPscript.js34How much the spring is held back: lower, the words go further past before they settle.
DROPscript.js14How far a word drops when it moves onto the next line, in pixels.
ROOMscript.js1.2How far the blank stays ahead of the writing, in em (the width of an M).
SHAKEscript.js90How hard a missing blank jolts on sending, in pixels per second.
WRITTENscript.js400The weight of what you write in the blanks.
SIGNEDscript.js600The weight what you wrote takes once the sentence is sent.
WAITscript.js0.8How many seconds the pretended sending takes.
SENTscript.jsThanks, {name}: we’ll write back within two days.The line beside the button once sent; {name} is the name written.

Try them in the Playground

When to use it

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.

On phones

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.

With reduced motion

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.

Pitfalls

  • With the blank exactly as wide as the text, at every letter the words after it started from their old place, that is on top of the new letter, and for a moment they read overlapped. Now the blank stays ROOM ahead of the writing and grows in steps: the words always move towards empty space.
  • A word wrapping onto the next line flew diagonally from one line to the other and covered its neighbours, which were making room for it meanwhile. Now it waits invisible for two tenths of a second, the time it takes the line to open, and then drops into the free space.
  • The arrows of the choices faded but kept their width, and the signed sentence kept a hole before the comma. Now their width is a CSS variable that drops to zero in the same tween as the weight, measured again inside relayout.
  • The springs at 0.62 damping went 8 per cent too far: on an 80-pixel move that was 6 pixels, and the word touched the full stop before it. At a DAMP of 34 they go one pixel too far.

Libraries

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.

Testing

Tested on 19 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: To do

License

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.

All components