Skip to content
animorio
IT
All components

The filling shelf

Loading, Physics, TransitionsGSAP, CSS onlyAdvancedCompressed weight: Demo 17 KBGSAP 28 KBFreeDownload the zip

The offline download of an audiobook app, made as a shelf between two brass bookends: every audiobook is a book. The one downloading appears in glass, tips, falls and leans on the row, and its cloth rises from the bottom with the bytes; downloaded, it stands up and slides into place with a little knock. If the connection drops the book slips down like a ladder down a wall; at the end a finger runs along the spines, a light passes over the gold and the bookcase lights up.

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, viewport-fit=cover">
  <title>The filling shelf</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bricolage-grotesque@5.3.0/standard.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the offline downloads of an audiobook app, as a shelf between two brass bookends: every audiobook is a book.
         The one downloading appears as a glass ghost, tips over and leans on the row, and fills with its cloth from
         the bottom as its bytes arrive; done, it stands up and slides into the row. The books are built by script.js
         from the list at the top of it -->
    <section class="shelf" aria-labelledby="shelf-title" data-shelf>
      <div class="shelf__head">
        <div class="shelf__status">
          <p class="shelf__phase"><span class="shelf__swap" data-phase>Not on this phone yet</span></p>
          <p class="shelf__note"><span class="shelf__swap" data-note>About 10 s on this Wi-Fi</span></p>
        </div>
        <p class="shelf__big" aria-hidden="true">
          <span class="shelf__swap" data-big>1.4 GB</span>
          <svg class="shelf__tick" viewBox="0 0 28 28" data-tick><path d="M6.5 14.8l5 5 10-11.4" pathLength="1"/></svg>
        </p>
      </div>
      <div class="shelf__case" role="progressbar" aria-labelledby="shelf-title" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" aria-valuetext="Not downloaded" data-bar>
        <div class="shelf__light" aria-hidden="true" data-light></div>
        <div class="shelf__row" aria-hidden="true" data-row>
          <span class="bookend bookend--start"></span>
          <span class="bookend bookend--end" data-end></span>
        </div>
        <div class="shelf__board" aria-hidden="true"></div>
      </div>
      <div class="shelf__foot">
        <div class="shelf__list">
          <h2 class="shelf__title" id="shelf-title">For the flight</h2>
          <p class="shelf__meta" data-meta>10 audiobooks · 22 h</p>
        </div>
        <div class="shelf__buttons">
          <button class="pill pill--icon" type="button" aria-label="Cancel download" data-cancel hidden>
            <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7.5 7.5l9 9M16.5 7.5l-9 9"/></svg>
          </button>
          <button class="pill" type="button" data-action data-autoclic><span class="pill__label" data-label>Download</span></button>
        </div>
      </div>
      <p class="sr" role="status" 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.

67 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 filling shelf” from Animorio and fit it into my site

What it does

On a white background, alone in the middle, a card in dark green like the inside of a painted bookcase, 400 pixels wide: at the top Not on this phone yet and About 10 s on this Wi-Fi, and on the right, large and narrow, 1.4 GB; in the middle an oak shelf running from one edge of the card to the other, with two brass bookends at its ends and nothing between them; below, For the flight with 10 audiobooks · 22 h and the Download button. Pressed, the button becomes an outlined Pause, the cross to cancel appears next to it and the first book, Salt Road, appears in glass by the bookend on the left: a clear book with its title and bands blind stamped, which comes down a few pixels as if it were being set down, tilts to the point where it balances, hesitates, then falls and leans on the bookend with a little bounce. When the first bytes arrive (Connecting, then Downloading Salt Road) its red cloth rises from the bottom with a line of light along its edge, and the gold title with it; at the top the percentage counts, below it 2 of 10 books · about 6 s left, and next to the list’s name the megabytes downloaded, 412 MB of 1.4 GB. When the book is full it stands up sliding towards the row, as if pushed, knocks against its neighbour, rocks for a moment on its other corner and stops; meanwhile the next book appears in glass behind it, falls and leans on it. Every book has its colour, its thickness (its audiobook’s), its height and its spine: gold on red, blue, purple and green cloth, a black label on the yellow, dark ink on the sage, on the pink and on the ivory with red bands; long titles go on two lines or close up on the width axis of the typeface. The connection comes in fits and starts and sometimes stalls for a moment, and the cloth rises and stops with it. Pause stops everything (Paused, 3 of 10 books are on this phone, Resume); the cross takes the books off the shelf, one after another from the right, and Download canceled is left. One time in two, in the demo, at about 58% the connection drops: the book that was downloading, just set down, slips with its foot to the right and its top down along the book before, like a ladder sliding down a wall, until it is lying almost flat; its glass turns red and at the top Couldn’t download Hollow Bells, The connection dropped appears, with the Try again button, which stands it back up and starts the bytes again. The last book drops straight into the last slot, exactly, between the row and the bookend; then a finger runs along the spines from the left (every book rises a few pixels and drops back), a light passes over the gold of the titles, the wall behind the books lights up warm, the percentage gives way to a tick that draws itself and the words become Ready to listen offline, 10 of 10 books · downloaded in 9 s, 1.4 GB on this phone. Remove downloads takes the books away and the shelf is empty again.

How it works

A book that falls

A book is a rigid rectangle t thick and h tall that turns about the corner it stands on. Its weight gives it an angular acceleration θ'' = 3g(h·sin θ − t·cos θ) ÷ 2(t² + h²), negative under the balance angle atan(t/h) (the book goes back upright) and positive above it (it falls). That is why the book is set down half a degree past its balance, with the smallest push: it hesitates, speeds up and knocks. The sums run on GSAP’s ticker in four substeps, with the frame limited to 1/30 of a second; at the knock the speed turns round and shrinks by BOUNCE.

The contact with the book before

With its foot a distance g from the row, the angle the book leans at comes from the contact: either its face touches the top corner of the book before (tan θ = g ÷ H, if the book is tall enough to reach it), or its top corner touches that book’s side (sin θ = g ÷ h). Whichever comes first. The same function serves while it leans, while it stands up sliding towards the row (g goes to zero and the angle with it) and while it slips away in the error: the foot moves right until the book is at 64 degrees or touches the bookend, just like a ladder sliding down a wall. So a book never goes through another one.

Where it rests, and the rocking

The book downloading leans at LEAN degrees, but always at least 7 degrees past its balance (a thick book has to lean further not to stand back up); if there is no room between the row and the bookend to lean like that, it stands upright next to the row, and the last one drops straight into its slot. Standing up it knocks against its neighbour with the speed it had and rocks on its other corner: the pivot changes, gravity brings it back, the neighbour stops it and takes some of its swing at every knock, and past 0.6 of its balance it is held back.

The bytes and the cloth

The pretend download is a timeline of arrivals of random size and interval with a stall or two; the books follow the bytes with a delay of FOLLOW seconds, but close the last stretch at a steady pace, otherwise an exponential chase would stay at 99%. The cloth is a layer over the glass cut from the bottom with clip-path: inset(), whose cut reaches 12 pixels past the sides to let its shadow through. If the books fall behind (each has almost a second of motion of its own) they hurry by 1.6 or 2.2 times, and an error only shows once the book that did not arrive is down on the shelf.

The spines and the light at the end

The spines are divs with writing-mode vertical-rl; a title too long for its spine first closes up a little (the sans ones on the wdth axis of Bricolage Grotesque), then, on a thick book, goes on two lines cut at the space nearest the middle if the halves are balanced, and only last gets smaller. The gold is a gradient with background-clip: text. At the end the same band of light crosses every book: each spine has its own copy of the gradient shifted by where the book stands (--bx), so it reads as one light; then the wall behind the books lights up with a warm gradient that spreads from the middle.

Values to change

NameFileDefaultWhat it changes
SECONDSscript.js8How long the download takes in the demo, in seconds (a real one follows its bytes).
BOOKSscript.js[['Salt Road', 142], ['Night Ferry', 96], …]The audiobooks from the left: the title on the spine and the size in megabytes, which also makes the book’s thickness. From 2 to 14.
FAIL_EVERYscript.js2In the demo, one download in how many loses its connection (0 never).
FAIL_ATscript.js0.58How far the download that goes wrong gets, from 0 to 1.
STALLSscript.js1How often the connection stalls for a moment: 0 never, 1 now and then, 2 often.
LEANscript.js30How far the book downloading leans on the row, in degrees (always at least 7 past the point where it would stand back up).
GRAVITYscript.js3400How hard the books fall over, in pixels a second squared.
BOUNCEscript.js0.32How much a book bounces off the one it lands on: 0 not at all, 0.6 a lot.
FOLLOWscript.js0.25How long the books take to catch up with the bytes, in seconds.

Try them in the Playground

When to use it

For an audiobook, ebook, podcast or music app that downloads a list to listen to or read offline, for a digital library or a comics app, and in general for a queued download of several items that have a name, where it is worth seeing which ones have already arrived.

On phones

The card is 400 pixels wide and on a phone takes the width of the screen less 16 pixels a side (358 at 390): the books get thinner to fill the shelf and the titles fit their spines again. The buttons are 44 tall and work by touch; the pill that swells under the pointer is only there with a mouse. Below 390 pixels a very long error message ends in an ellipsis.

With reduced motion

The books appear already upright in their place and the cloth fades in as the bytes arrive: no falling, no rocking, no slipping in the error (the book’s glass turns red). At the end no finger on the spines and no band of light: the bookcase light comes on with a fade and the tick appears. The words change with a fade.

Pitfalls

  • In a timeline a fromTo() draws its starting state at once: the old words vanished instead of sliding away, because the new ones’ fromTo (opacity 0) was already applied. It needs immediateRender: false.
  • gsap.ticker.time stops while GSAP’s ticker sleeps: the preview, which leaves the full shelf on show for at least 0.8 seconds before emptying it, waited forever. The times come from performance.now().
  • gsap.killTweensOf(book) stops the tweens but not the calls inside the book’s timeline: a book taken off the shelf went back to falling. Every book keeps its timeline and its call to the next book, and they stop together.
  • The clip-path that raises the cloth also cuts its shadow: the cut has to reach past the sides with negative values, inset(… -12px -12px -12px).
  • With a very high LEAN and low gravity the book standing up came in so fast it fell over the other way and through the shelf: the rocking is held at 0.6 of the balance, and in the Playground LEAN goes up to 45.
  • An exponential chase never arrives: the percentage stayed at 99% for over a second and every book waited for its last per cent. The last fractions close at a steady pace.
  • Every book has almost a second of motion of its own: with small books the shelf fell two books behind and the error named a book that had not appeared yet. Books that are behind hurry, and the error waits until the book is down.
  • With only two books the proportional thickness made them two boxes 150 pixels wide: a book is never thicker than 48 and the bookend on the right moves in.

Libraries

GSAP 3.15.0, Bricolage Grotesque (font variabile) 5.3.0, Fraunces (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