Skip to content
animorio
IT
All components

The field that counts its letters

Forms, Text, SVGGSAPAdvancedCompressed weight: Demo 12 KBGSAP 28 KBFreeDownload the zip

In the minimal style: the bio of a profile, 160 characters, with no box round it. Under the text a hairline is the room that is left: it gets shorter with every letter and ends at the count of characters left, in tabular figures that grow heavier as the limit comes near without ever changing width. Past the limit the extra letters fade, an orange hairline strikes them through one after the other, an upright mark shows where the limit falls and the count goes below zero; deleting them, the line comes back out of the count on a spring. Saving, a darker line runs up to the count and the pill says Saved.

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 field that counts its letters</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/archivo@5.3.0/wdth.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the short bio of a profile, 160 characters. No box round the field: its underline is the room that is left,
         a hairline that gets shorter with every letter and ends at the count. Past the limit the extra letters are
         struck through with a hairline of the accent and the count goes below zero; deleting them, the line grows
         back on a spring. The marks over the text and the line are drawn by the script -->
    <form class="bio is-waiting" action="#" novalidate data-bio data-autoclic>
      <div class="bio__head">
        <label class="bio__label" for="bio-text" data-label>Bio</label>
      </div>

      <div class="bio__box" data-box>
        <div class="bio__mirror" aria-hidden="true" data-mirror></div>
        <textarea class="bio__input" id="bio-text" name="bio" rows="3" maxlength="400" spellcheck="false" autocomplete="off" placeholder="A line or two about you" aria-describedby="bio-count bio-note" data-input>Type designer in Porto. I draw display faces for small presses and restore old wood type for anyone who still prints by hand.</textarea>
        <svg class="bio__marks" aria-hidden="true" data-marks>
          <path class="bio__wash" data-wash d=""/>
          <path class="bio__strike" data-strike d=""/>
          <path class="bio__limit" data-limit d=""/>
        </svg>
        <span class="bio__caret" aria-hidden="true" data-caret></span>
      </div>

      <div class="bio__gauge" data-gauge>
        <svg class="bio__rule" aria-hidden="true" data-rule>
          <path class="bio__track" data-track d=""/>
          <path class="bio__line" data-line d=""/>
          <path class="bio__ticks" data-ticks d=""/>
          <path class="bio__run" data-run d=""/>
        </svg>
        <p class="bio__count" id="bio-count" data-count><span class="bio__num" aria-hidden="true" data-num>35</span><span class="sr" data-count-said>35 characters left</span></p>
      </div>

      <div class="bio__foot">
        <p class="bio__note" id="bio-note" data-note><span class="note__line">Shown under your name on your profile.</span></p>
        <button class="save" type="submit" data-save>
          <span class="save__bg" aria-hidden="true"></span>
          <span class="save__labels">
            <span class="save__label is-on" data-word="save">Save</span>
            <span class="save__label" aria-hidden="true" data-word="saving">Saving</span>
            <span class="save__label" aria-hidden="true" data-word="saved">Saved</span>
          </span>
        </button>
      </div>
      <span class="sr" aria-live="polite" data-live></span>
    </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.

26 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 field that counts its letters” from Animorio and fit it into my site

What it does

On a white page the word Bio, small and grey; under it the bio of a profile already written on three lines (Type designer in Porto…), with no box round it. Under the text runs a hairline with a small tick at each end, like a dimension line in a technical drawing: it is the room that is left, and it ends at the number 35, the characters still free. To the left of the line, almost white, the part of the measure already used. Lower down Shown under your name on your profile. and the black Save pill. Tapping the field the word Bio turns black and fuller and the line darkens. Every letter typed takes its share off the line from the left, on a calm spring, and the left tick feels the knock; the number goes down and gains weight, a little at first and a lot near the limit, but its figures stay the same width, so the end of the line never trembles. At zero the line is used up and only one tick is left, orange. Past it, the extra letters fade and an orange hairline strikes them through one after the other, even across a line break; an upright orange mark rises at the exact place in the text where the limit falls, even halfway through a word, and the number goes negative, orange, with a little settle. Under it reads Too long to save. Trim the struck-out part. and the pill goes pale: pressed, the strike thickens for a moment and the sentence underneath turns orange. Deleting, the strike draws back towards the mark, the mark folds away and the line comes back out of the number with a push, goes a little past and settles. Pressing Save the pill says Saving, a darker line runs along the whole measure up to the number, the tick and the number feel it arrive, then the pill says Saved and under it reads Saved. Your profile shows it now.

How it works

An invisible twin that knows where every letter falls

The textarea is real and draws its own letters, caret and selection. Next to it, hidden with visibility: hidden, is a twin div with the same typeface, the same width, white-space: pre-wrap and overflow-wrap: break-word, holding the same text in a single node. For every character past the limit a Range on the twin, with getClientRects, gives the letter’s box on its line, even after a line break. Characters are counted the way the eye counts them (codePointAt, so an emoji is one), and the limit is turned into the right place inside the text. The twin also tells how tall the text is: the textarea takes that height (no scrollbar) and the box follows it with GSAP.

The strike, the veil and the limit mark

Over the textarea, in an SVG with no viewBox (one unit is one pixel), three paths are rewritten on every frame from the letters’ boxes: for every line a rectangle in the page colour at VEIL opacity that fades the extra letters, a 1.5-pixel hairline at the height of the small letters, and an upright mark where the limit falls (between two letters, or in the middle of a space). The strike does not jump from line to line: a spring carries the number of struck letters towards the extra ones, and the letter halfway is struck for its share. Spaces at the end of a line are left out.

The springs, made by hand

The two ends of the line, the weight of the number, the strike, the mark and the little knocks of the ticks are damped springs integrated by hand on the GSAP ticker: acceleration equal to minus omega squared times the distance, minus two zeta omega times the speed, with the step capped at 1/30 of a second and split into steps of 1/240. When the line gets shorter the spring has almost no bounce (zeta 0.86); when it gets longer it is GIVE, and when the extra letters are gone the left end gets a push of PUSH pixels a second to the left, so the line comes out of the number, goes a little past and settles. The loop falls asleep once every spring is still and no tween is moving the colours.

Figures that gain weight without growing wider

The number is set in Archivo with tabular figures (tnum). Its weight goes from LIGHT to HEAVY on a curve, the share used raised to the power CURVE, so it is felt mostly near the limit. But tabular figures widen with weight too: when the typeface arrives the script measures ten figures at every weight, from 100 to 900 in steps of 50, and finds by bisection the value of the wdth axis that makes them as wide as at the lightest weight. While it moves, wght and wdth change together from that table, and the end of the line moves only when the number of figures changes. Below zero it is the real minus sign (U+2212, written with String.fromCharCode).

Saving, and the preview

Save with a bio too long does not save: the pill has aria-disabled, the strike thickens on a spring and the sentence under the field turns orange. With a bio that fits, a 2-pixel line runs along the whole measure in WAIT seconds; when it arrives the tick and the number’s weight get a push, the pill goes from Saving to Saved (three words in the same grid cell, so its width never changes) and the aria-live region says so. Cmd or Ctrl with Enter saves from the keyboard. A click from a script on the form (detail 0, the library’s preview) plays one whole round on a GSAP timeline, without taking the focus and with a drawn caret: a sentence added until it runs over, a pause on the struck letters, the extra part deleted, Save, Saved and the bio as it was.

Values to change

NameFileDefaultWhat it changes
LIMITscript.js160How many characters the bio can have.
LIGHTscript.js220The weight of the count while there is plenty of room.
HEAVYscript.js820Its weight at the limit and past it.
CURVEscript.js2.6How late the count gains weight: 1 steadily from the start, 4 only in the last stretch.
GIVEscript.js0.45How springy the line is when it grows back: 0.25 bounces, 1 does not bounce at all.
PUSHscript.js480The push the line gets when the extra letters are gone, in pixels a second.
VEILscript.js0.6How pale the letters past the limit go behind their strike, 0 to 1.
WAITscript.js0.9How many seconds saving takes in the demo, while the darker line runs.
DONEscript.jsSaved. Your profile shows it now.The line under the field once the bio is saved.

Try them in the Playground

When to use it

Wherever a short text with a limit is written: the bio of a profile, the description of a shop or an event, the caption of a post, the summary for search engines, a message for a gift card. The line says how much room is left without reading the number, and the strike shows exactly which letters are out, instead of cutting them off without a word as maxlength does.

On phones

The field takes the width of the screen (358 pixels on a 390 phone), the text drops to 18 pixels on 27-pixel lines, the count to 20 and the pill to 42. The box keeps room for four lines, so the page does not jump when the text wraps; beyond that it grows on a spring. A tap opens the keyboard: every letter shortens the line as on a computer, and the strike follows the extra letters even when they wrap. The pill swells under the finger as under the pointer.

With reduced motion

No springs: the line, the count’s weight, the strike and the limit mark are already in place at every letter, with no travel and no bounce. Only the colours, the line under the field and the words on the pill change with a fade, one after the other. Saving waits WAIT seconds without the running line, then the pill says Saved.

Pitfalls

  • Tabular figures are the same width as each other, but not from one weight to another: with weight the number grew wider and the end of the line trembled at every letter. Now a table measured when the typeface arrives says, for every weight, how far to narrow the wdth axis so that ten figures stay as wide as at the lightest weight.
  • The textarea growing with the text pushed the whole form up, because the page keeps it in the middle: on the fourth line the field jumped by 15 pixels. Now the box keeps room for four lines and only grows beyond that, on a spring.
  • The drawn caret of the preview was orange like the limit mark: at the end of the struck text they looked like two limit marks. The real caret and the drawn one are now black, and the orange is left only to what is past the limit.
  • The line under the field said how many letters were too many and changed at every key: deleting quickly, the new lines had no time to appear and an old number stayed on screen. Now there is one sentence only, and the count says the number.
  • The three words on the pill swapped at the same time and for a moment Save and Saving read on top of each other: now the new one comes in once the old one has almost gone, with reduced motion too.

Libraries

GSAP 3.15.0, Archivo (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 12 KB; the libraries: GSAP 28 KB.

Testing

Tested on 16 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