
The three-line menu
Menus, Transitions, Physics, Text
NewFree
The search of a cooking site where the words typed sit in the middle of the field and every recipe suggested slides sideways until the same letters sit right under them: the letters found make a column, marked by a highlighter stroke running down from the field through the list. New recipes unroll out of their letters, the ones that stop matching roll back into them, and the one taken rises into the field, landing on the letters typed.
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 column search</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/alegreya@5.3.0/wght.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/alegreya@5.3.0/wght-italic.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the search of a cooking site. The words typed sit in the middle of the field, and every recipe suggested
slides sideways until the same letters sit right under them: the letters found make a column, marked down
the list like a highlighter. A new recipe unrolls from its letters, a chosen one rises into the field.
The rows are made by the script from the list at the top of script.js -->
<div class="seek" data-seek data-autoclic>
<form class="seek__form" role="search" action="#" novalidate data-form>
<div class="seek__field" data-field>
<span class="seek__swipe" aria-hidden="true" data-swipe></span>
<svg class="seek__lens" viewBox="0 0 24 24" aria-hidden="true"><path d="M10.5 4.5a6 6 0 1 0 0 12a6 6 0 1 0 0-12zM15 15l4.6 4.6"/></svg>
<input class="seek__input" id="seek-input" type="text" inputmode="search" enterkeyhint="search" role="combobox" aria-label="Search recipes" aria-autocomplete="list" aria-expanded="false" aria-controls="seek-list" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" placeholder="Search recipes" data-input>
<span class="seek__caret" aria-hidden="true" data-caret></span>
<button class="seek__clear" type="button" aria-label="Clear the search" tabindex="-1" data-clear>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7.5 7.5l9 9M16.5 7.5l-9 9"/></svg>
</button>
<button class="seek__go" type="submit" aria-label="Search" aria-disabled="true" data-go>
<span class="seek__arrow" aria-hidden="true" data-arrow><svg viewBox="0 0 24 24"><path d="M5 12h13M12.5 6.5 18 12l-5.5 5.5"/></svg></span>
</button>
</div>
<div class="seek__panel" data-panel>
<div class="seek__rows" data-rows>
<span class="seek__band" aria-hidden="true" data-band></span>
<span class="seek__beam" aria-hidden="true" data-beam></span>
<ul class="seek__list" id="seek-list" role="listbox" aria-label="Recipes" data-list></ul>
</div>
<div class="seek__none" data-none hidden>
<p class="none__what" data-none-what></p>
<p class="none__tip" data-none-tip></p>
</div>
</div>
<p class="seek__note" data-note><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M4 10h11M10.5 5.5 15 10l-4.5 4.5"/></svg><span data-note-words></span></p>
<p class="sr" role="status" aria-live="polite" data-said></p>
</form>
<div class="seek__flight" aria-hidden="true" data-flight></div>
<!-- the highlighter's edge: not ruled, but a little uneven, like a real marker's -->
<svg class="seek__defs" aria-hidden="true" focusable="false">
<filter id="seek-marker" x="-10%" y="-4%" width="120%" height="108%">
<feTurbulence type="fractalNoise" baseFrequency="0.03 0.07" numOctaves="2" seed="7" result="grain"/>
<feDisplacementMap in="SourceGraphic" in2="grain" scale="2.4" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</svg>
</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>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.
60 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 column search” from Animorio and fit it into my siteThe input is real: role combobox, aria-expanded, aria-controls pointing at the list (role listbox) and aria-activedescendant on the recipe picked, which has aria-selected. It has text-align center and the same room on both sides, so the words typed sit in the middle; the small cross that empties it takes the lens’s place, on the left, so as not to steal room. The width of the words is measured with a hidden copy in the input’s own font (getBoundingClientRect, with the decimals); when they are wider than the field they start from its edge and the scroll (scrollLeft) is taken off.
search() compares the letters regardless of capitals and accents: every letter becomes a single letter (é becomes e), so a place in the plain name is the same place in the real one. For each recipe it keeps the best place, the start of the name, then the start of a word, then inside a word, and for a tie the order of RECIPES; it shows SHOWN of them, with the letters found in a mark. When nothing matches, each word typed that is in no name is compared with the words of the recipes using the Levenshtein distance (one letter for short words, two from six letters up).
For each row the script measures where the mark sits inside the name (its getBoundingClientRect less the name’s) and moves the row sideways by the middle of the words typed less the middle of the mark: the letters found land exactly under the ones in the field, to a hundredth of a pixel. The highlighter is a single element, as wide as the widest mark, with an SVG filter (feTurbulence and feDisplacementMap) that makes its edge uneven like a marker’s, and mix-blend-mode multiply, so over the grey band it darkens like real ink.
Each row’s position, sideways and up and down, rides on two springs (SPRING: stiffness and damping) integrated in steps of 1/240 of a second in GSAP’s ticker, which stops drawing once everything is still; typing fast the springs only change their target, without starting over. Between the old list and the new one the script finds the longest increasing subsequence of the rows that stay: those slide in their order and never cross, the others leave and come back in their new place. The ones that stay wait 0.08 seconds for the ones leaving to fade, and the new rows come in once the others have almost stopped.
A new row comes in with a clip-path inset() that at first shows only the mark and opens to the whole name in UNROLL seconds (power3.out): it seems to come out of its letters, to the left and to the right. A row leaving does the opposite in 0.16 seconds. At the opening the rows come one after the other while the yellow stroke draws itself from the top (scaleY); when nothing matches it goes back up into the field.
The name taken is copied into a layer above everything, at the exact spot where it is, and rises in half of LIFT seconds: its letters found are already under the ones typed, so they land on them without moving sideways. Then it slides into the middle of the field, the mark thins from 700 to 420 in weight (the font is variable) and the highlighter, measured again on every frame, follows it, widening from the letters found to the whole name; on arrival the input takes the name and the copy goes. A click from a script (detail 0, the library’s preview) types without taking the focus, with a drawn caret, a step per click: ri, then s and the arrows, the recipe taken, a typo and the close word, pe and the search.
| Name | File | Default | What it changes |
|---|---|---|---|
SHOWN | script.js | 6 | How many recipes the list shows at most. |
ROW | script.js | 48 | The height of a row, in pixels. |
SPRING | script.js | [420, 30] | The spring that keeps the rows on the column: stiffness, and damping. |
UNROLL | script.js | 0.45 | The seconds a new recipe takes to unroll out of its letters. |
LIFT | script.js | 0.7 | The seconds the recipe taken takes to rise into the field and settle in the middle. |
TRIES | script.js | ['lemon', 'saffron', 'pear'] | The ingredients offered when nothing matches and no word is close. |
RECIPES | script.js | ['Saffron risotto', 'Lemon ricotta pancakes', …] | The recipes to search, the most cooked first. |
For the search of a recipe site, a shop, a bookshop, a product’s documentation: wherever the names to find are short and it helps to see at once why a result was found, even when the letters searched for sit in the middle of the name. The search() function is the only one to change to ask a server for the results.
The field takes the width of the screen minus 32 pixels and stays 60 tall; the words are 17 pixels (from 16 up iPhone does not zoom the page when the field is tapped) and the keyboard shows the search key. The rows are 48 pixels tall for the finger and are taken by tapping them, without the field losing the focus; with the cross in the lens’s place there are 240 pixels to type in, and the names in the list fit whole even with the column in the middle.
No springs, no unrolling and no flights: the rows fade in and out already in their place on the column, the yellow stroke fades in instead of running down, the band jumps from row to row and the lens fades into the cross. The recipe taken fades from the list and the name appears in the field; the arrow does not leave the square, it dims and lights up again. The column, the advice and the keys work the same.
GSAP 3.15.0, Alegreya (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 15 KB; the libraries: GSAP 28 KB.
Tested on 6 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.