Skip to content
animorio
IT
All components

The crop mark tabs

Tabs, Transitions, HoverGSAPAdvancedCompressed weight: Demo 9.1 KBGSAP 28 KBFreeDownload the zip

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.

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

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.

59 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 crop mark tabs” from Animorio and fit it into my site

What it does

On a white background a row of five filters in a variable grotesque, grey and light, each with its small number of projects up high: All 6, Identity 3, Editorial 2, Packaging 2, Motion 2. The chosen one is black and heavier, with a vermilion number, and around its corners stand four vermilion crop marks, two thin lines per corner set off from the edge, as on printed sheets. Below, six projects in hairline frames: the disciplines and the year at the top, the name at the bottom, each set with its own width and weight, like a type specimen. Choosing another filter the four marks leave one after the other, those on the side of the new tab first: on the way they open a little and their horizontal lines stretch with the speed, then they close in around the new word going past by a pixel or two and click into register with a small knock towards the corner. The new word takes weight going a hair too far and settles, the old one goes light again. The projects that stay slide to their new places, those that leave fade where they are, those that arrive come up one after the other while their name widens into its own shape; in the frames the chosen discipline turns black. With the mouse over a filter the marks on its side lean towards it; pressing, they tighten on the word.

How it works

The crop marks

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

Four springs

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 staggered start

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.

Weight without pushing

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.

The projects with FLIP

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.

Keyboard and preferences

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.

Values to change

NameFileDefaultWhat it changes
MARKscript.js[10, 5]The crop marks: how long each line is and how far it stands from the corner, in pixels.
STIFFNESSscript.js210How quick the marks are (the stiffness of the springs).
DAMPINGscript.js0.8The damping of the springs: at 1 they land without overshooting, lower they overshoot more.
SPREADscript.js0.3How different the four springs are from one another (0 = all the same).
STAGGERscript.js0.05The seconds between one corner leaving and the next.
PULLscript.js4How far the marks lean towards a filter under the mouse, in pixels.
WEIGHTscript.js[360, 660]The weight of the filters and that of the chosen one.
CARDSscript.js0.75The seconds the projects take to slide to their new places.

Try them in the Playground

When to use it

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.

On phones

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.

With reduced motion

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.

Pitfalls

  • The growing weight widens the word and pushes all the other tabs at every change: under each word there is an invisible copy at the heaviest weight, in the same grid cell, that holds the width.
  • A row that scrolls sideways clips everything that sticks out, at the top and bottom too: the marks live inside the row, and its room above and below is worked out from the length of the marks.
  • The ResizeObserver answers right after the page opens and put the marks back in place, skipping the entrance: they are put back only when they are still and have arrived.
  • With FLIP the frames that leave, left in the grid, still take up their place: they are taken out with position absolute where they were seen, behind the others with isolation and z-index -1, and only then is the new place measured.
  • A change halfway through another: what is seen is measured (with the transforms and opacity of that moment), the animations are stopped and everything starts again from there, so nothing jumps. The springs of the marks only change their goal and bend their path.

Libraries

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.

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