Skip to content
animorio
IT
All components

The table with pockets

Tables, Transitions, SVGGSAPAdvancedCompressed weight: Demo 17 KBGSAP 28 KBFreeDownload the zip

A coffee roastery’s wholesale orders, with a filter where every state is a pocket. Changing the filter, the new orders come out of their pocket and open in their places, between the rows already there, which move aside; then every order that no longer belongs closes in on its status label, the label flies into its pocket, slips behind the edge and the pocket takes it with a move of its own (the flame flares, the bag puffs up, the van bounces), and the table tightens. Never empty, still in under a second. Opened, an order shows the roast curve of its batch, drawn in the colour of the beans at every temperature.

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 table with pockets</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bricolage-grotesque@5.3.0/standard.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the table with pockets: a coffee roastery's wholesale orders. Every status tab is a pocket: filtering, the
         orders that do not belong close in on their status label and the label flies into its pocket; choosing a
         pocket, its orders come out and open up in their places. An order opens on the roast curve of its batch -->
    <section class="orders" aria-labelledby="orders-title" data-orders data-autoclic>
      <header class="orders__head">
        <h2 class="orders__title" id="orders-title">Wholesale orders</h2>
        <p class="orders__note">Roasted this week</p>
      </header>

      <div class="tabs" role="group" aria-label="Show orders" data-tabs>
        <span class="tabs__knob" aria-hidden="true" data-knob></span>
        <button class="tab" type="button" aria-pressed="true" data-filter="all">
          <span class="tab__in">
            <span class="tab__label">All</span>
            <span class="tab__count" data-count></span>
          </span>
        </button>
        <button class="tab tab--roasting" type="button" aria-pressed="false" data-filter="roasting">
          <span class="tab__in">
            <span class="tab__icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 3.2c1.3 3.4 5.8 5.6 5.8 11a5.8 5.8 0 0 1-11.6 0c0-2.8 1.5-4.6 2.9-5.8.2 1.8.9 2.8 1.9 3.2-.5-3 .1-5.8 1-8.4z"/><path d="M12 20.2a2.5 2.5 0 0 1-2.5-2.5c0-1.5 1.2-2.4 2-3.6.4 1.1 1.1 1.7 1.8 2.1.7.4 1.2 1 1.2 1.6a2.5 2.5 0 0 1-2.5 2.4z"/></svg></span>
            <span class="tab__label">Roasting</span>
            <span class="tab__count" data-count></span>
          </span>
        </button>
        <button class="tab tab--packed" type="button" aria-pressed="false" data-filter="packed">
          <span class="tab__in">
            <span class="tab__icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M6.3 8.6h11.4v10.6a1.4 1.4 0 0 1-1.4 1.4H7.7a1.4 1.4 0 0 1-1.4-1.4z"/><path d="M6.9 8.6 7.8 3.8h8.4l.9 4.8"/><path d="M10 6.2h4"/><circle cx="12" cy="13.6" r="1.9"/></svg></span>
            <span class="tab__label">Packed</span>
            <span class="tab__count" data-count></span>
          </span>
        </button>
        <button class="tab tab--shipped" type="button" aria-pressed="false" data-filter="shipped">
          <span class="tab__in">
            <span class="tab__icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M2.8 6.4h11v10.2h-11z"/><path d="M13.8 9.6h3.9l3.5 3.6v3.4h-7.4"/><circle cx="7" cy="17.4" r="1.9"/><circle cx="17.2" cy="17.4" r="1.9"/></svg></span>
            <span class="tab__label">Shipped</span>
            <span class="tab__count" data-count></span>
          </span>
        </button>
      </div>

      <div class="sheet" data-sheet>
        <table class="table" aria-labelledby="orders-title" data-table>
          <thead>
            <tr>
              <th class="c-cafe" scope="col">Café</th>
              <th class="c-coffee" scope="col">Coffee</th>
              <th class="c-kg num" scope="col">Weight</th>
              <th class="c-total num" scope="col">Total</th>
              <th class="c-state" scope="col">Status</th>
            </tr>
          </thead>
          <tbody data-rows></tbody>
        </table>
      </div>

      <div class="flight" aria-hidden="true" data-flight></div>
      <p class="sr" aria-live="polite" data-live></p>
    </section>
  </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.

114 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 table with pockets” from Animorio and fit it into my site

What it does

On a white background Wholesale orders and Roasted this week, then a row of four pockets on a stone-coloured track, All, Roasting, Packed and Shipped, each with its count (7, 2, 2, 3) and the last three with their drawn icon: an ember flame, a kraft paper coffee bag, a dark green van. A white pill sits under the pocket chosen. Under it, a real table with Café, Coffee, Weight, Total and Status: seven made-up orders from cafés and bakeries (Little Owl Café, Tamarind House, Copper Pot, Bramble & Bean, Kiosk Nord, Halcyon Bakery, Saltmarsh Deli), the coffee’s origin, the kilos, the total in euros and the state in a tinted label with its icon. Choosing a pocket the pill slides under it. First the labels of the orders coming come out: they come down from their pocket to their rows’ places, between the rows already there, which move aside to make room, and there the window opens again from the label to the whole row. As soon as the last one has landed, every order that does not belong closes in on its label, like a rounded window tightening round it and taking its tint, and the label sets off upwards at once, bends towards its pocket leaning that way with a growing shadow, and goes in straight from below, vanishing behind the track’s edge. The pocket takes it: its words jump a little, the count swells and the icon makes its move (the flame stretches up, the bag squashes, the van jolts and tilts). Then the rows that stay slide up to close the gaps, all together. The table is never empty and everything is still in under a second. Clicking an order (or its row) the little arrow turns and its roast opens underneath: the bean temperature curve draws itself from the left, falls from 200 degrees to the turning point and climbs back ever more slowly, with the temperature written at its tip and under it an area going from the green of raw beans to yellow, cinnamon and browns, down to the near black of a dark roast; passing 196 degrees the dashed First crack line appears. Beside it Roast, Drop, In the cup and Delivery. For an order still in the drum the curve stops at now, with the temperature and now, and the rest of the plan appears dashed.

How it works

Going through both

Every order has a place in the table: row (its real row), wait (the place kept, unseen, while its label comes), hold (the place kept while the window closes), free (the place empty, the label has gone) and none (in its pocket). When the filter changes the table goes through the union of both lists, in the order of the data: the places of the orders coming appear, unseen (visibility hidden), between the rows already there, which slide to make room with a transform on the tr. The orders coming leave their pocket at once; the ones leaving stay real rows until the last new one is about to land (and never past the moment the first one starts to open, so there is always a row to read). Then, once, the empty places go and the rows move up. The two flocks never fly together.

The window and the flight

A row leaving is replaced, in the same frame, by a copy (the row cloned into a table with the same columns, read from the heads into a colgroup, and its roast if it was open) inside a window: a div with overflow hidden whose edges go from the row’s to the label’s in CLOSE seconds, while the copy fades and the background takes the label’s tint. The label flies along a cubic Bézier with its first control point straight above the start and the second straight below the pocket (SWING of the climb): it leaves and arrives vertically, tilts up to 8 degrees towards where it goes, lifts towards us up to 28% bigger with its shadow, in FLY seconds (more or less with the distance), speeding up towards the pocket and slowing down towards the row.

The pocket

The layer of the labels in flight is above the table and below the pockets (z-index): the label passes behind the track’s bottom edge and vanishes there, with no fade. The pocket takes it: its insides jump 3 pixels with a back.out in 0.22 seconds (a filter is used many times in a row), the count swells, and the icon makes its move, the flame stretches up from its base, the bag squashes, the van jolts tilting on its back wheels. The moves are on the svg, the button’s press (scale 0.96) on the button: one element for GSAP and one for the CSS.

Interrupting, and the timings

A click halfway takes every order on from where it is: a label that was coming goes back to its pocket (its place is freed), one that was leaving goes back to its place (if its window was still closing, it opens again there), a row that had not left yet is decided again; if a label’s destination has not changed, it goes on its way. A click on a row during the change waits until the table is still. With the starting values, measured frame by frame: the first new rows can be read between 250 and 333 milliseconds, the table is still within 750 and everything, pockets included, within 900.

The roast curve

Built at the real width of its box, so its words keep their size. The bean temperature is a cubic Hermite curve through the charge (200 degrees), the turning point (92 degrees at 1:15), the yellowing (150), the first crack (196) and the drop, with the slopes taken from the rates of rise around them, which therefore slow down as in a real drum; a point every 4 seconds. At every frame it is drawn again up to q of the way: the line, the area under it, a hairline and the temperature at its tip. The area has a gradient with a stop every twenty-fourth of the time, in the colour of the bean at that moment’s temperature (green, yellow, cinnamon at the first crack, browns), and a mask that lightens it towards the bottom. For an order still in the drum the curve stops at now and the rest of the plan appears dashed.

A real table, keys and the preview

A table with th scope col and scope row, the café’s name is a button with aria-expanded and aria-controls on the detail row, the pockets are buttons with aria-pressed in a group (the arrows go from one to the next), an aria-live region says Showing 2 packed orders, Esc closes the details. The table keeps the height of all its rows (min-height), so what comes after never jumps. A click from a script (the library’s preview) goes round: Shipped and opens Copper Pot, Packed, All, Roasting and opens Tamarind House, closes and All.

Values to change

NameFileDefaultWhat it changes
ORDERSscript.js[['Little Owl Café', 'Huila, Colombia', 12, 318, 'packed'], …]The orders: café, coffee, kilos, total in euros and state (roasting, packed or shipped).
ROASTSscript.js[['Medium', '8:52', '10:40', 212, 'Red apple, panela', 'Thu 8 Oct'], …]Each order’s roast: level, first crack, drop (minutes:seconds), drop temperature, what is in the cup, delivery and, for the ones still in the drum, how long they have been in it.
FLYscript.js0.28The seconds of a label’s flight into its pocket, or out of it.
CLOSEscript.js0.12The seconds a row takes to close in on its label (and a little more to open out of it).
STAGGERscript.js0.025The seconds between one order setting off and the next.
SWINGscript.js0.45How far a label climbs straight up before bending towards its pocket, as a share of the climb: 0 is a straight line.
DRAWscript.js1.3The seconds the roast curve takes to draw itself.

Try them in the Playground

When to use it

For a wholesaler’s orders, a warehouse’s shipments, a practice’s cases, a help desk’s tickets, the applications to a job: any list split by state, where it helps to see where the rows the filter hides have gone.

On phones

At 390 pixels the Coffee and Weight columns go and move under the café’s name (the region and the kilos); the labels narrow to 86% with the width axis of Bricolage Grotesque, the rows are 62 pixels tall and the four pockets share the width, 44 tall, with their words and counts (the icons stay in the labels). The roast goes above its facts. The animations are the same: the labels climb the column of the states up to the pockets.

With reduced motion

Nothing flies and nothing slides: the rows that leave disappear, the ones that come fade in in their places, the pill is at once under the pocket chosen and the roast opens already drawn, with a fade.

Pitfalls

  • A table row cannot be cut by a window, nor fly: the label in flight is a copy of the row in a layer above, in a table with the same column widths (read from the heads into a colgroup) and the same classes; the real row becomes hidden in the same frame, so the hand-over does not show.
  • The copy of an open order takes its roast along too: in the copy the ids of the gradient and of the mask are renamed, or url(#...) would point at the original, which at the same moment goes display none (and there gradients do not paint).
  • A row coming back must have its place before it shows: visibility hidden on the tr (not display none), so the others move aside while the label is still flying.
  • An SVG element’s opacity attribute loses against a CSS rule with opacity: the dashed line of the plan and the cursor’s hairline keep their transparency in stroke-opacity, so the script turns them on and off with the attribute.
  • In an exchange (Packed to Shipped) the table stayed empty for almost half a second between the rows leaving and the ones coming: now it goes through the union of both lists, the new ones open between the old ones and the old ones leave only after. The moment they leave is tied to CLOSE (the first new one opening, minus 0.55 CLOSE, plus 30 milliseconds), or with very fast windows one or two frames had no rows.
  • With the pockets’ elastic tails (0.7 seconds) a change took more than a second and a half: a filter is used many times in a row, so everything was shortened (flight 0.28, window 0.12, jolts 0.22 with back.out).
  • The detail row of an order leaving while open must keep its place, unseen, until the table tightens: the visibility hidden rule only covered .row, and the roast stayed there uncovered after its window had closed.
  • On a phone the four pockets with their icons did not fit in 358 pixels: the icons go, the words and counts stay.
  • A click on a row during the change of filter would have opened the roast while the places move, and the labels in flight would have landed in the wrong place: the click waits until the table is still (under a second).

Libraries

GSAP 3.15.0, Bricolage Grotesque (font variabile) 5.3.0.

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

Testing

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