Skip to content
animorio
IT
All components

The search that weighs the letters

Search, Text, TransitionsGSAP, CSS onlyAdvancedCompressed weight: Demo 13 KBGSAP 28 KBFreeDownload the zip

In the minimal style: the catalogue search of a small type foundry. As you type, in the names that match the letters found take weight one after another, on a measured spring, and each one narrows just enough to keep its width: the names never shift sideways. The entries that no longer match thin out to a hairline and leave, the others close up on real springs, and the count is in tabular figures that roll.

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 search that weighs the letters</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/archivo@5.3.0/standard.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the catalogue search of a small type foundry. Typing, the letters found in every name take weight one after
         another, while each letter narrows just enough to keep its width: the names never move sideways. The rows
         that stop matching thin out and leave, the others ride on springs to their new places. The rows are made by
         the script from the list at the top of script.js -->
    <div class="weigh" data-search data-autoclic>
      <div class="weigh__field" data-field>
        <svg class="weigh__lens" viewBox="0 0 24 24" aria-hidden="true"><path d="M10.5 4.75a5.75 5.75 0 1 0 0 11.5a5.75 5.75 0 1 0 0-11.5zM14.75 14.75l4.75 4.75"/></svg>
        <input class="weigh__input" id="weigh-input" type="text" inputmode="search" enterkeyhint="search" role="combobox" aria-label="Search typefaces" aria-autocomplete="list" aria-expanded="true" aria-controls="weigh-list" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" placeholder="Search the catalogue" data-input>
        <span class="weigh__caret" aria-hidden="true" data-caret></span>
        <span class="weigh__count" aria-hidden="true"><span class="weigh__num" data-num></span><span class="weigh__unit" data-unit>typefaces</span></span>
        <button class="weigh__clear" type="button" aria-label="Clear the search" tabindex="-1" data-clear>
          <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 8l8 8M16 8l-8 8"/></svg>
        </button>
        <span class="weigh__rule" aria-hidden="true"></span>
      </div>

      <div class="weigh__room" data-room>
        <span class="weigh__band" aria-hidden="true" data-band></span>
        <ul class="weigh__list" id="weigh-list" role="listbox" aria-label="Typefaces" data-list></ul>
        <div class="weigh__none" data-none hidden>
          <p class="none__word" aria-hidden="true"><span class="none__letters" data-none-letters></span><span class="none__strike" data-strike></span></p>
          <p class="none__what">No typeface in the catalogue matches.</p>
          <p class="none__try">Try <span data-tries></span></p>
        </div>
      </div>

      <p class="sr" role="status" aria-live="polite" data-said></p>
    </div>
  </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.

57 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 search that weighs the letters” from Animorio and fit it into my site

What it does

On a white background a search field made only of a lens, the words and a thin rule, with the count on the right, 12 typefaces. Below, six typeface names in a light grotesque, each with its class and how many styles it has. On arrival the names grow from a hairline weight to their own, letter by letter. With the focus in the field the rule turns black from the lens onwards. Typing, the letters searched for are found in order even when they are not side by side, as in a command palette: in every name those letters turn black and heavy one after another, with a small overshoot and back, and narrow just enough not to widen the name. The other letters turn grey. The names that no longer match thin out and fade, the others move up on a spring to close the gaps, and the digits of the count roll. The arrows or the pointer bring a grey band onto a name, whose letters found turn vermilion, with the Enter key drawn beside it; Enter or a click picks it: the name goes into the field and turns all heavy, left to right. Escape empties the field and the list goes back as it was. If nothing matches, the word typed appears large, loses its weight letter by letter, a thin line strikes it and two words to try are offered below, taking weight under the pointer. With nothing typed, the name under the pointer takes just a little weight.

How it works

Measuring the letters

Once the face is loaded, the script sets every letter of the catalogue at a grid of 9 weights by 9 widths (the wght axis from 100 to 900, the wdth axis from 62 to 125) in a hidden box and measures its width with getBoundingClientRect, in ems. Kerning is off (font-kerning: none), so each letter’s width is its own and the sum gives the whole name.

Weight that does not widen

For any weight holdWidth() interpolates the row of the grid between the two nearest weights and solves the width on the wdth axis that keeps the letter as wide as at rest (REST at wdth 100). It does so on every frame of the GSAP tween carrying the weight, so also in the spring’s overshoot: measured in the browser, the name stays the same width to a hundredth of a pixel.

Finding the letters

match() looks for the letters typed, in order, inside the name with a small dynamic programme: letters side by side, at the start of a word and early in the name score more, a long gap scores less. It keeps the best placement, and those are the letters that take weight, one after another every STAGGER seconds, on a damped spring curve written as a GSAP ease function.

Rows on springs

The rows are positioned absolutely and each has a real spring (the stiffness and damping of SPRING, the step capped at 1/30 of a second and split into steps of 1/240), which keeps its speed if the target changes while it moves. They stay in the catalogue’s order: a letter more only takes rows away and the others close up, a letter less brings them back. The ones leaving thin out to FAINT and fade in 0.16 seconds; the ones staying start a moment later, and new rows wait for the others to have gone past. The loop stops when everything is still.

Keys, count and the empty state

The field has role combobox, aria-expanded and aria-activedescendant on the row picked, which has role option and aria-selected; the arrows move the band (on a spring too), Enter picks, Escape empties, and a hidden aria-live line says how many typefaces there are. The digits of the count change one by one from the right, rolling up or down. With no results the word typed loses weight letter by letter and a line strikes it; the words in TRIES replace it in the field.

The preview’s tour

A click from a script (detail 0, the library’s preview) starts a GSAP timeline about 10 seconds long with a pretend caret and without taking the focus: it types ser, goes down a row, picks, types mo, then zq which finds nothing, and finally Escape brings everything back to rest. A real touch or key stops it at once.

Values to change

NameFileDefaultWhat it changes
TYPEFACESscript.js[['Esker Sans', 'Sans', 12], ['Ashlar Grotesk', 'Sans', 9], …]The catalogue: name, class and number of styles, the most licensed first.
SHOWNscript.js6How many typefaces the list shows at most.
RESTscript.js300The weight of the names at rest (100 to 900).
HITscript.js780The weight a letter found takes (100 to 900).
FAINTscript.js110The weight of the names that leave and of a word that finds nothing.
STAGGERscript.js0.07The seconds between one letter found and the next taking weight.
SETTLEscript.js0.6The seconds a letter takes to take its weight and settle.
SPRINGscript.js[420, 32]The spring that carries the rows to their places: stiffness and damping.
TRIESscript.js['serif', 'mono']The words offered when nothing matches.

Try them in the Playground

When to use it

For the search of a type foundry, a catalogue, a shop with few products, a product’s documentation or a command palette: wherever the names are short, you search as you type and it helps to see at once which letters found the result. The search() function is the only one to change to ask a server for the results.

On phones

The field takes the width of the screen minus 32 pixels; the words in the field are 17 pixels (from 16 up iPhone does not zoom the page when it is tapped) and the keyboard shows the search key. The names drop to 20 pixels and the rows stay 52 tall for the finger: a tap brings the band onto the row and picks it. The drawn Enter key is not there, as a phone does not need it; everything else, weights, springs and count, is the same.

With reduced motion

No springs and no rolling: the rows fade in and out already in their place, the band jumps from row to row, the digits of the count change with a fade and the focus rule lights up instead of drawing. The letters found change weight all together in a fifth of a second, with no wave and no overshoot, and keep their width; the line striking the word not found is already in place.

Pitfalls

  • A letter taking weight widens and pushes all the following ones: the name shook at every letter typed. Now every letter’s width is measured at 81 combinations of weight and width, and on every frame the wdth axis that keeps it as wide as at rest is solved; measured, the name stays the same to a hundredth of a pixel even halfway through the animation.
  • With kerning on, a letter’s width depends on its neighbour and the sums were wrong: the names have font-kerning: none, like the box they are measured in.
  • With the results sorted by score, at every letter the rows swapped places and two names passed over each other, unreadable. Now they stay in the catalogue’s order, so a letter more only takes rows away; if search() reorders them, a row that must pass another dims on its way and lights up again on arrival.
  • New rows came in right where a row was moving up to close a gap: now they wait 0.16 seconds when any row moves more than half a row, and the ones leaving fade in 0.16 seconds instead of 0.2 with power2.in, which kept them visible to the end.
  • The library’s preview clicks every 2.8 seconds, but the tour lasts about 10: every click restarted it halfway. Now clicks from a script are ignored while the tour’s timeline is running, and a real touch or key stops it.

Libraries

GSAP 3.15.0, Archivo (font variabile, assi wght e wdth) 5.3.0.

Compressed with gzip, the three demo files weigh 13 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