Skip to content
animorio
IT
Back to the library

The tabbed folders

ScrollCSS only, GSAPEasyCompressed weight: Demo 6.3 KBGSAP + ScrollTrigger 45 KBFreeDownload the zip

Scroll inside the preview

Five folders that pile up as you scroll: each one stops at the same height, and the tabs, each one place further along, stay in a row like the dividers of a file drawer. Click a tab to go back to its folder.

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 tabbed folders</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/anybody@5.3.0/wdth.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/geist-mono@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header class="top">
    <p class="brand">Offcut</p>
    <p class="top__line">Selected jobs</p>
  </header>

  <main>
    <section class="intro">
      <h1 class="intro__title">Five jobs,<br>filed.</h1>
      <p class="intro__text">Every job at Offcut lives in a folder. Scroll, and each one slides over the one before; the tabs stay in a row, so you can always pull a folder back out.</p>
    </section>

    <!-- every folder sticks at the same height; its tab sits one place further along, so the row fills up -->
    <ul class="drawer" data-drawer style="--count: 5">
      <li class="folder" data-folder style="--n: 0; --tint: #f7f5ee; --on: #0e1440">
        <div class="folder__row"><button class="folder__tab" type="button" data-tab aria-label="Go to Ferry timetable"><span>Ferry</span></button></div>
        <article class="folder__body">
          <div class="folder__inside">
            <div class="folder__text">
              <p class="folder__date">March 2026</p>
              <h2 class="folder__title">Ferry timetable</h2>
              <p class="folder__line">Forty crossings a day, set so you can read them from the pier.</p>
              <ul class="folder__tags"><li>Timetable</li><li>Wayfinding</li></ul>
            </div>
            <figure class="folder__art" aria-hidden="true">
              <svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice">
                <rect width="400" height="300" fill="#2343e6"/>
                <circle cx="306" cy="80" r="38" fill="#ffd21f"/>
                <rect x="212" y="112" width="24" height="36" fill="#0e1440"/>
                <rect x="212" y="120" width="24" height="8" fill="#ffd21f"/>
                <rect x="132" y="146" width="126" height="34" fill="#ff5b4a"/>
                <g fill="#f7f5ee"><rect x="146" y="156" width="16" height="13"/><rect x="172" y="156" width="16" height="13"/><rect x="198" y="156" width="16" height="13"/><rect x="224" y="156" width="16" height="13"/></g>
                <path d="M84 180 H312 L284 222 H112 Z" fill="#f7f5ee"/>
                <path d="M0 228 q25 -16 50 0 t50 0 t50 0 t50 0 t50 0 t50 0 t50 0 t50 0 V300 H0 Z" fill="#1a31b4"/>
                <path d="M-25 256 q25 -16 50 0 t50 0 t50 0 t50 0 t50 0 t50 0 t50 0 t50 0 t50 0 V300 H-25 Z" fill="#0e1440"/>
              </svg>
              <i></i><i></i><i></i><i></i>
            </figure>
          </div>
        </article>
      </li>

      <li class="folder" data-folder style="--n: 1; --tint: #7df0c4; --on: #0e1440">
        <div class="folder__row"><button class="folder__tab" type="button" data-tab aria-label="Go to Seed packets"><span>Seeds</span></button></div>
        <article class="folder__body">
          <div class="folder__inside">
            <div class="folder__text">
              <p class="folder__date">January 2026</p>
              <h2 class="folder__title">Seed packets</h2>
              <p class="folder__line">A packet for every seed in the neighbourhood library, printed in two inks.</p>
              <ul class="folder__tags"><li>Packaging</li><li>Riso</li></ul>
            </div>
            <figure class="folder__art" aria-hidden="true">
              <svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice">
                <rect width="400" height="300" fill="#f7f5ee"/>
                <g transform="rotate(-11 122 156)">
                  <rect x="70" y="66" width="104" height="172" rx="6" fill="#0e1440"/>
                  <g transform="translate(122 150)" fill="#7df0c4"><rect x="-3" y="-6" width="6" height="52"/><path d="M0 6 C-30 2 -36 -30 -36 -30 C-8 -32 0 -14 0 6 Z"/><path d="M0 -6 C26 -10 32 -40 32 -40 C8 -40 0 -24 0 -6 Z"/></g>
                  <rect x="86" y="212" width="72" height="8" fill="#f7f5ee"/>
                </g>
                <g transform="rotate(3 200 150)">
                  <rect x="148" y="52" width="104" height="184" rx="6" fill="#ff5b4a"/>
                  <g transform="translate(200 146)" fill="#0e1440"><rect x="-3" y="-6" width="6" height="56"/><path d="M0 8 C-30 4 -36 -28 -36 -28 C-8 -30 0 -12 0 8 Z"/><path d="M0 -4 C26 -8 32 -38 32 -38 C8 -38 0 -22 0 -4 Z"/></g>
                  <rect x="164" y="210" width="72" height="8" fill="#f7f5ee"/>
                </g>
                <g transform="rotate(15 282 160)">
                  <rect x="230" y="70" width="104" height="172" rx="6" fill="#ffd21f"/>
                  <g transform="translate(282 154)" fill="#0e1440"><rect x="-3" y="-6" width="6" height="52"/><path d="M0 6 C-30 2 -36 -30 -36 -30 C-8 -32 0 -14 0 6 Z"/><path d="M0 -6 C26 -10 32 -40 32 -40 C8 -40 0 -24 0 -6 Z"/></g>
                  <rect x="246" y="216" width="72" height="8" fill="#0e1440"/>
                </g>
              </svg>
              <i></i><i></i><i></i><i></i>
            </figure>
          </div>
        </article>
      </li>

      <li class="folder" data-folder style="--n: 2; --tint: #0e1440; --on: #f7f5ee">
        <div class="folder__row"><button class="folder__tab" type="button" data-tab aria-label="Go to Night market"><span>Market</span></button></div>
        <article class="folder__body">
          <div class="folder__inside">
            <div class="folder__text">
              <p class="folder__date">October 2025</p>
              <h2 class="folder__title">Night market</h2>
              <p class="folder__line">Posters that glow under the lamps: one for each of the six nights.</p>
              <ul class="folder__tags"><li>Posters</li><li>Screen print</li></ul>
            </div>
            <figure class="folder__art" aria-hidden="true">
              <svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice">
                <rect width="400" height="300" fill="#1a31b4"/>
                <path d="M318 34 a46 46 0 1 0 34 78 a36 36 0 1 1 -34 -78 Z" fill="#ffd21f"/>
                <path d="M0 92 Q200 150 400 80" fill="none" stroke="#f7f5ee" stroke-width="2.5"/>
                <g fill="#ffd21f"><rect x="52" y="106" width="16" height="24" rx="5"/><rect x="122" y="120" width="16" height="24" rx="5"/><rect x="192" y="125" width="16" height="24" rx="5"/><rect x="262" y="118" width="16" height="24" rx="5"/></g>
                <g>
                  <rect x="46" y="200" width="7" height="72" fill="#f7f5ee"/><rect x="166" y="200" width="7" height="72" fill="#f7f5ee"/>
                  <path d="M30 168 H190 L176 204 H44 Z" fill="#ff5b4a"/>
                  <g fill="#f7f5ee"><path d="M62 168 H94 L90 204 H64 Z"/><path d="M126 168 H158 L156 204 H130 Z"/></g>
                  <rect x="40" y="244" width="140" height="30" fill="#f7f5ee"/>
                </g>
                <g>
                  <rect x="232" y="212" width="7" height="60" fill="#f7f5ee"/><rect x="348" y="212" width="7" height="60" fill="#f7f5ee"/>
                  <path d="M216 182 H372 L358 216 H230 Z" fill="#7df0c4"/>
                  <g fill="#0e1440"><path d="M248 182 H278 L274 216 H250 Z"/><path d="M310 182 H340 L338 216 H314 Z"/></g>
                  <rect x="226" y="250" width="136" height="24" fill="#f7f5ee"/>
                </g>
              </svg>
              <i></i><i></i><i></i><i></i>
            </figure>
          </div>
        </article>
      </li>

      <li class="folder" data-folder style="--n: 3; --tint: #ffd21f; --on: #0e1440">
        <div class="folder__row"><button class="folder__tab" type="button" data-tab aria-label="Go to Paper zoo"><span>Zoo</span></button></div>
        <article class="folder__body">
          <div class="folder__inside">
            <div class="folder__text">
              <p class="folder__date">June 2025</p>
              <h2 class="folder__title">Paper zoo</h2>
              <p class="folder__line">A book to cut up: eleven animals, and not a drop of glue.</p>
              <ul class="folder__tags"><li>Book</li><li>Die cut</li></ul>
            </div>
            <figure class="folder__art" aria-hidden="true">
              <svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice">
                <rect width="400" height="300" fill="#f7f5ee"/>
                <path d="M70 44 H330 V262 H70 Z" fill="none" stroke="#0e1440" stroke-width="2.5" stroke-dasharray="9 8"/>
                <path d="M112 74 L200 158 L112 196 Z" fill="#ff5b4a"/>
                <path d="M288 74 L200 158 L288 196 Z" fill="#ff5b4a"/>
                <path d="M112 196 L200 158 L288 196 L200 248 Z" fill="#e8402f"/>
                <path d="M150 204 L200 182 L250 204 L200 248 Z" fill="#f7f5ee" stroke="#0e1440" stroke-width="0"/>
                <path d="M186 232 H214 L200 248 Z" fill="#0e1440"/>
                <path d="M150 150 l18 10 l-18 8 Z" fill="#0e1440"/>
                <path d="M250 150 l-18 10 l18 8 Z" fill="#0e1440"/>
              </svg>
              <i></i><i></i><i></i><i></i>
            </figure>
          </div>
        </article>
      </li>

      <li class="folder" data-folder style="--n: 4; --tint: #ff5b4a; --on: #0e1440">
        <div class="folder__row"><button class="folder__tab" type="button" data-tab aria-label="Go to Open Tuesday"><span>Tuesday</span></button></div>
        <article class="folder__body">
          <div class="folder__inside">
            <div class="folder__text">
              <p class="folder__date">April 2025</p>
              <h2 class="folder__title">Open Tuesday</h2>
              <p class="folder__line">Signs for a repair café that opens one evening a week.</p>
              <ul class="folder__tags"><li>Signage</li><li>Stencil</li></ul>
            </div>
            <figure class="folder__art" aria-hidden="true">
              <svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice">
                <rect width="400" height="300" fill="#0e1440"/>
                <path d="M136 92 q-14 -16 0 -32 M170 92 q-14 -16 0 -32 M204 92 q-14 -16 0 -32" fill="none" stroke="#f7f5ee" stroke-width="7" stroke-linecap="round"/>
                <path d="M226 144 h20 a28 28 0 0 1 0 56 h-20" fill="none" stroke="#f7f5ee" stroke-width="15"/>
                <path d="M104 116 H236 V206 a30 30 0 0 1 -30 30 H134 a30 30 0 0 1 -30 -30 Z" fill="#f7f5ee"/>
                <g transform="rotate(34 300 176)">
                  <rect x="289" y="120" width="22" height="132" rx="7" fill="#ffd21f"/>
                  <circle cx="300" cy="118" r="28" fill="#ffd21f"/>
                  <rect x="290" y="82" width="20" height="38" fill="#0e1440"/>
                </g>
              </svg>
              <i></i><i></i><i></i><i></i>
            </figure>
          </div>
        </article>
      </li>
    </ul>

    <section class="outro">
      <p class="outro__text">Next job: yours.</p>
    </section>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/ScrollTrigger.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.

62 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 tabbed folders” from Animorio and fit it into my site

What it does

The jobs of an invented studio, each in its folder: paper, mint, midnight blue, yellow, coral. As you scroll, the first folder stops near the top; the second slides over it and covers it, but its tab sits one place to the right, and the first one’s stays in view. And so on to the fifth: at the end the five tabs are in a row, in steps, with the edges of the older folders showing behind the new one. While it is being covered, a folder loses its content; when a folder lands, its tab gives a small jump and the handles of the frame around the picture appear. Every tab is a button: it takes the page back to its folder.

How it works

All at the same height

The folders are the items of a list, one after the other, each with position: sticky and the same top. When one reaches the line it stops; the next one, which comes later in the code and therefore sits above, keeps rising and covers it. Between one and the next there is a 14vh margin: the stretch of scroll in which the whole folder is in view.

The tabs in a row

Each folder is a row with the tab and, under it, the body. The tab is a fifth of the folder wide and has a left margin of as many fifths as --n says: 0 for the first, 4 for the last. The rest of the row is empty, and the tabs of the folders underneath show through it. The slanted side is a triangle cut with clip-path.

The pile that holds

A sticky element has to stay inside the content of its container, margin included. That is why the list ends with an empty stretch, an ::after 36vh tall: until that stretch has gone by, all the folders stay where they are, and then they leave together.

What the script adds

One number tied to the scroll with ScrollTrigger: how many folders have landed on the first one, fractions included, from 0 to 4. From it, for each folder, how many are on top of it: it rises 7 px for each (the --rise variable), and while the first of them slides over it its content goes down to 15% opacity (--fade). The scrub lags by 0.4 seconds.

Landing, and the tabs

When the number passes a whole, the folder that has landed is the one in front: its tab jumps 9 px and falls back on an elastic, the picture’s four handles grow one after the other, and the tab’s label lights up. A click on a tab works out where its folder stops (the first folder’s point, plus as many distances as there are folders before it) and takes the page there, with the browser’s smooth scroll.

Values to change

NameFileDefaultWhat it changes
--count, --nindex.html5, 0-4How many folders there are and each one’s place in the row: change them together when you add or remove one.
--tint, --onindex.html#f7f5ee, #0e1440Each folder’s colour and the colour of its text.
--top, --tab, --slantstyle.css58px, 46px, 22pxWhere the folders stop, how tall the tabs are and how wide their slanted side is.
LIFTscript.js7The pixels a folder rises for every folder that lands on it.
FADEscript.js0.85How much of its content a covered folder loses: 1 makes it vanish altogether.
SCRUBscript.js0.4The seconds the pile takes to catch up with the scroll.
.folder margin, .drawer::afterstyle.css14vh, 36vhThe scroll between one folder and the next, and how long the pile holds at the end.

Try them in the Playground

When to use it

For a short list of jobs, case studies, services or chapters, three to six of them, where it matters to see at once how many there are and to be able to go back: a studio’s portfolio, the index of a report, the stages of a journey.

On phones

The same, with narrower tabs and 11 px labels; inside the folder the picture goes under the text and takes the height that is left.

With reduced motion

The folders pile up all the same, because it is the page that scrolls, but they do not rise, the content does not fade and the tabs do not jump; a click on a tab goes to the folder without animated scrolling.

Pitfalls

  • Padding at the bottom of the list is not enough to hold the pile: a sticky box has to stay inside the container’s content, and padding is not content. It takes a real empty stretch, such as an ::after.
  • If the last folder does not have the same margin as the others, the others are one margin taller than it and get pushed up just as it lands.
  • The tab’s row is as wide as the folder even where it is empty: without pointer-events: none on the folder (and auto on tab and body) clicks never reach the tabs of the folders underneath.
  • A transform on the folder does not move the point where it stops: translateY can be used to raise it. Changing top, instead, would change the pile.
  • offsetTop of a sticky box is where it is now, not where it started: the distance between two folders has to be worked out from height plus margin.
  • The whole folder must fit on screen, tab included: the body’s height is the smaller of 540 px and the screen less the sticky line, the tab and a margin.

Libraries

GSAP 3.15.0, ScrollTrigger (GSAP) 3.15.0, Anybody (font variabile) 5.3.0, Geist Mono (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 6.3 KB; the libraries: GSAP + ScrollTrigger 45 KB.

Testing

Tested on 30 September 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 animations