Skip to content
animorio
IT
All components

The bookmark tabs

Tabs, Transitions, HoverGSAPMediumCompressed weight: Demo 5.1 KBGSAP 28 KBFreeDownload the zip

Four tabs under a ruled line, and a red cloth ribbon hanging from the line above the chosen one: changing tab, the ribbon rolls up, slides along the line and drops above the new one, swinging, while the text comes in from the side it comes from.

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 bookmark tabs</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/newsreader@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the tabs: a ribbon hangs from the top rule above the chosen tab. Choosing another one, the ribbon rolls up,
         slides along the rule and drops above it, swinging; the text below comes in from the side of the new tab -->
    <div class="book" data-book>
      <div class="book__tabs" role="tablist" aria-label="About the house" data-tabs>
        <span class="ribbon" aria-hidden="true" data-ribbon><span class="ribbon__cloth" data-cloth></span></span>
        <button class="tab" type="button" role="tab" id="tab-rooms" aria-controls="page-rooms" aria-selected="true">The rooms</button>
        <button class="tab" type="button" role="tab" id="tab-breakfast" aria-controls="page-breakfast" aria-selected="false" tabindex="-1">Breakfast</button>
        <button class="tab" type="button" role="tab" id="tab-arrival" aria-controls="page-arrival" aria-selected="false" tabindex="-1">Getting here</button>
        <button class="tab" type="button" role="tab" id="tab-rules" aria-controls="page-rules" aria-selected="false" tabindex="-1">House rules</button>
      </div>
      <div class="book__pages" data-pages>
        <section class="page" role="tabpanel" id="page-rooms" aria-labelledby="tab-rooms" tabindex="0">
          <h2>Six rooms under old beams</h2>
          <p>Each one has its own bathroom, linen sheets and a window that opens onto the vines. The two on the top floor share a small terrace for the evening.</p>
        </section>
        <section class="page" role="tabpanel" id="page-breakfast" aria-labelledby="tab-breakfast" tabindex="0" hidden>
          <h2>Served until half past ten</h2>
          <p>Bread from the village oven, eggs from the hens behind the barn and jams made in August. In good weather the table goes outside, under the fig tree.</p>
        </section>
        <section class="page" role="tabpanel" id="page-arrival" aria-labelledby="tab-arrival" tabindex="0" hidden>
          <h2>Twenty minutes from the station</h2>
          <p>Take the road towards the hills and turn left after the stone chapel. If you come by train, write to us and we will meet you on the platform.</p>
        </section>
        <section class="page" role="tabpanel" id="page-rules" aria-labelledby="tab-rules" tabindex="0" hidden>
          <h2>Few rules, kindly kept</h2>
          <p>Check-in is from three in the afternoon and check-out by eleven. Quiet after midnight, dogs welcome, and please close the gate behind you.</p>
        </section>
      </div>
    </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.

23 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 bookmark tabs” from Animorio and fit it into my site

What it does

On a white background a thin black line and below it four tabs in a book typeface: The rooms, Breakfast, Getting here and House rules; the chosen one is black and heavier, the others grey. From the line hangs a red cloth ribbon with a V-cut tail, above the chosen tab. Choosing another tab, the ribbon rolls up into the line, slides along it leaning back as if dragged, and drops above the new one, stretching a little past its length; then it swings forward with the momentum and settles. The title and text of the old tab leave towards the side being left, those of the new one come in one after the other from the side of the new tab, and the height in between changes smoothly. Under the mouse the ribbon leans towards the tab pointed at. When the page opens the ribbon drops in and the first text rises.

How it works

The line and the ribbon

The tabs sit in a row with a top border one and a half pixels thick; the ribbon is an absolute span at the top of the row, turning around its top edge. Inside it is the cloth: a red rectangle with a dark fold at the top, shading at the sides and the V-cut tail made with clip-path, which rolls up and down with scaleY from its top edge.

Where the ribbon goes

The ribbon goes to the middle of the chosen tab, measured each time with offsetLeft and offsetWidth; a ResizeObserver puts it back in place when the row changes size, for example when the typeface arrives.

The change

One timeline: the cloth goes to scaleY 0.12 in 0.18 seconds, the ribbon slides in SLIDE seconds with power3.inOut leaning back by 8 degrees, the cloth drops with back.out(DROP[1]) in DROP[0] seconds, and the ribbon swings up to SWING degrees forward and back to zero with elastic.out. A new choice halfway stops the timeline and starts another one from wherever the ribbon is.

The text

The old title and text move out by 28 pixels as they fade, the new ones come in from 40 pixels on the other side with power3.out in TEXT seconds, one after the other; meanwhile the container goes from the old height to the new one. The component is held from the top of the page, so when its height changes only its bottom moves.

Keyboard and phone

It is a real tablist: role tab and tabpanel, aria-selected and a single tab reachable with the Tab key; the arrows, Home and End change tab and bring the focus along. On a narrow screen the row scrolls sideways, fades on the side where more tabs are, and the chosen one is brought into view.

Values to change

NameFileDefaultWhat it changes
DROPscript.js[0.55, 1.6]The ribbon dropping: the seconds, and how much it stretches past its length.
SWINGscript.js10How far the ribbon swings when it lands, in degrees.
SLIDEscript.js0.42The seconds the rolled-up ribbon takes to slide to the new tab.
LEANscript.js6How far the ribbon leans towards a tab under the mouse, in degrees.
TEXTscript.js0.6The seconds the new title and text take to come in.

Try them in the Playground

When to use it

For the information of a holiday house, a product or a service split into a few parts: the rooms, breakfast, how to get there. Three to six short tabs; with more the row scrolls.

On phones

The row of tabs scrolls sideways if it does not fit, fading on the side where more tabs are, and the chosen tab is brought into view. Taps work like clicks; without a mouse the ribbon does not lean towards the tabs.

With reduced motion

The ribbon jumps straight above the chosen tab without rolling up or swinging, the text changes at once and when the page opens everything is already in place.

Pitfalls

  • Centred in the page, a component that changes height moves up and down as a whole at every change: it must be held from the top.
  • A row of tabs that does not wrap, inside a grid, widens the column past the screen: the column must be fixed with minmax(0, 1fr), so that the row scrolls instead.
  • With quick changes the text animations pile up: at every change the ones running are stopped and the pages that have nothing to do with it are hidden.
  • The ribbon moves with x and leans with rotation, and the cloth that rolls up is an element inside it: that way the three never get in each other’s way.

Libraries

GSAP 3.15.0, Newsreader (font variabile) 5.3.0.

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

Testing

Tested on 3 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: Done

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