
The inchworm menu
Menus, Hover, Transitions
NewFree
In the minimal style: the filters of a portfolio, All, Identity, Editorial, Packaging and Motion. The chosen tab is not underlined but framed by four vermilion crop marks, like a sheet ready for the guillotine: changing tab, the four corners leave one after the other, each on its own spring, and close in again around the new word, which takes weight; below, the projects slide to their new places.
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 crop mark tabs</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 filters of a design studio's work. The chosen filter is not underlined: four printer's crop marks stand
off its corners, as on a sheet ready for the guillotine. Choosing another, the four marks travel apart, each
on its own spring, and close in again around the new word; the projects below move to their new places
(FLIP), the ones that leave fade and the ones that arrive breathe in. The marks are drawn by the script -->
<div class="folio" data-folio>
<div class="filters" role="tablist" aria-label="Filter the work" data-tabs>
<svg class="marks" aria-hidden="true" data-marks><path data-marks-path /></svg>
<button class="tab" type="button" role="tab" id="tab-all" aria-controls="work" aria-selected="true" data-filter="all"><span class="tab__label"><span class="tab__word" data-word>All</span><sup class="tab__n">6</sup></span><span class="tab__ghost" aria-hidden="true"><span class="tab__word">All</span><sup class="tab__n">6</sup></span></button>
<button class="tab" type="button" role="tab" id="tab-identity" aria-controls="work" aria-selected="false" tabindex="-1" data-filter="identity"><span class="tab__label"><span class="tab__word" data-word>Identity</span><sup class="tab__n">3</sup></span><span class="tab__ghost" aria-hidden="true"><span class="tab__word">Identity</span><sup class="tab__n">3</sup></span></button>
<button class="tab" type="button" role="tab" id="tab-editorial" aria-controls="work" aria-selected="false" tabindex="-1" data-filter="editorial"><span class="tab__label"><span class="tab__word" data-word>Editorial</span><sup class="tab__n">2</sup></span><span class="tab__ghost" aria-hidden="true"><span class="tab__word">Editorial</span><sup class="tab__n">2</sup></span></button>
<button class="tab" type="button" role="tab" id="tab-packaging" aria-controls="work" aria-selected="false" tabindex="-1" data-filter="packaging"><span class="tab__label"><span class="tab__word" data-word>Packaging</span><sup class="tab__n">2</sup></span><span class="tab__ghost" aria-hidden="true"><span class="tab__word">Packaging</span><sup class="tab__n">2</sup></span></button>
<button class="tab" type="button" role="tab" id="tab-motion" aria-controls="work" aria-selected="false" tabindex="-1" data-filter="motion"><span class="tab__label"><span class="tab__word" data-word>Motion</span><sup class="tab__n">2</sup></span><span class="tab__ghost" aria-hidden="true"><span class="tab__word">Motion</span><sup class="tab__n">2</sup></span></button>
</div>
<section class="work" id="work" role="tabpanel" aria-labelledby="tab-all" tabindex="0" data-work>
<article class="card" data-tags="identity packaging" style="--wdth: 62; --wght: 820">
<p class="card__meta"><span class="card__tags"><span data-tag="identity">Identity</span> · <span data-tag="packaging">Packaging</span></span><span class="card__year">2026</span></p>
<h2 class="card__name" data-name>Corlett Bakery</h2>
</article>
<article class="card" data-tags="editorial" style="--wdth: 118; --wght: 280">
<p class="card__meta"><span class="card__tags"><span data-tag="editorial">Editorial</span></span><span class="card__year">2025</span></p>
<h2 class="card__name" data-name>Margin Review</h2>
</article>
<article class="card" data-tags="packaging" style="--wdth: 88; --wght: 520">
<p class="card__meta"><span class="card__tags"><span data-tag="packaging">Packaging</span></span><span class="card__year">2025</span></p>
<h2 class="card__name" data-name>Ambrel Tea</h2>
</article>
<article class="card" data-tags="identity motion" style="--wdth: 125; --wght: 860">
<p class="card__meta"><span class="card__tags"><span data-tag="identity">Identity</span> · <span data-tag="motion">Motion</span></span><span class="card__year">2026</span></p>
<h2 class="card__name" data-name>Volta Week</h2>
</article>
<article class="card" data-tags="editorial" style="--wdth: 72; --wght: 380">
<p class="card__meta"><span class="card__tags"><span data-tag="editorial">Editorial</span></span><span class="card__year">2024</span></p>
<h2 class="card__name" data-name>North Ledger</h2>
</article>
<article class="card" data-tags="motion identity" style="--wdth: 100; --wght: 200">
<p class="card__meta"><span class="card__tags"><span data-tag="motion">Motion</span> · <span data-tag="identity">Identity</span></span><span class="card__year">2025</span></p>
<h2 class="card__name" data-name>Orrery</h2>
</article>
</section>
</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.
59 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 crop mark tabs” from Animorio and fit it into my siteA single SVG path over the row of filters, redrawn on every frame: for each corner a horizontal and a vertical line that extend the sides of the tab, MARK[0] long and set MARK[1] pixels off the corner, on half coordinates and with shape-rendering crispEdges, so they stay one sharp pixel.
Each corner is a point with a position and a velocity, pulled by a spring towards the corner of the chosen tab (offsetLeft, offsetTop, offsetWidth and offsetHeight, read at every step). Stiffness STIFFNESS and damping DAMPING, different for each corner by SPREAD; it is integrated on the GSAP ticker in four sub-steps, with the step capped at 1/30 of a second, and when all four are still the ticker lets them go.
The corners on the side of the new tab leave at once, the others STAGGER seconds apart, with gsap.delayedCall: on the way the frame stretches towards the new tab and then closes again. Speed opens the marks by a few pixels and stretches the horizontal lines; when the four arrive a small spring on the distance from the corner gives a knock inwards, the click into register.
Under its word each filter has an invisible copy at the heaviest weight: the tab is always as wide as its chosen word, so the growing weight never moves the others. The weight is a number written into font-variation-settings, with wght and a hair of wdth: it goes 40 past WEIGHT[1] and comes back.
Before the change, where every frame is seen is measured (getBoundingClientRect, transforms included), then the filter changes, the new place is measured and every frame that stays starts with x and y from the difference and slides to zero in CARDS seconds. Those that leave are taken out of the grid with position absolute where they were seen and fade behind the others; the grid keeps the height of all the projects, so what is below never moves.
It is a real tablist: role tab and tabpanel, aria-selected, a single tab reachable with the Tab key; the arrows, Home and End change filter and bring the focus along. On a narrow screen the filters condense with the wdth axis to fit in one row.
| Name | File | Default | What it changes |
|---|---|---|---|
MARK | script.js | [10, 5] | The crop marks: how long each line is and how far it stands from the corner, in pixels. |
STIFFNESS | script.js | 210 | How quick the marks are (the stiffness of the springs). |
DAMPING | script.js | 0.8 | The damping of the springs: at 1 they land without overshooting, lower they overshoot more. |
SPREAD | script.js | 0.3 | How different the four springs are from one another (0 = all the same). |
STAGGER | script.js | 0.05 | The seconds between one corner leaving and the next. |
PULL | script.js | 4 | How far the marks lean towards a filter under the mouse, in pixels. |
WEIGHT | script.js | [360, 660] | The weight of the filters and that of the chosen one. |
CARDS | script.js | 0.75 | The seconds the projects take to slide to their new places. |
For the filters of the work of a studio, a photographer or an agency, or for the categories of a catalogue or a blog: three to six short words above a grid. The crop mark speaks of print and graphic design, so it suits above all the sites of people who make images.
The filters condense with the wdth axis of the typeface and all fit in one row; if they did not the row scrolls sideways and brings the chosen one into view. The grid goes to two columns. A tap works like a click and, while pressing, the marks tighten on the word as with the mouse.
The crop marks fade out and back in already around the new tab, without travelling; the grid changes with a fade, without sliding, and when the page opens everything is already in place.
GSAP 3.15.0, Archivo (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 9.1 KB; the libraries: GSAP 28 KB.
Tested on 16 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.