Skip to content
animorio
IT
Back to the library

The coffee that fills as you scroll

Scroll, SVGGSAPAdvancedCompressed weight: Demo 13 KBGSAP + ScrollTrigger 45 KBLenis 5.3 KBFreeDownload the zip

Scroll inside the preview

A roastery’s page: as you scroll, a cup seen at three quarters fills up. First the espresso falls in a thin thread from the two spouts and the crema rises, then a milk jug slides in, tilts and pours, rocks and draws a leaf on the surface, and at the end the steam rises in curls. Scroll back up and the cup empties.

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="it">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Il caffè allo scroll</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/full.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/mona-sans@5.3.0/standard.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <!-- la testata della torrefazione inventata -->
  <header class="testata">
    <p class="marchio">Ashgrove <span>Roasters</span></p>
    <nav class="menu" aria-label="Main">
      <a href="#tazza">The cup</a>
      <a href="#fine">Beans</a>
      <a href="#fine">Visit</a>
    </nav>
  </header>

  <main>
    <!-- l'apertura -->
    <section class="apertura">
      <p class="apertura__etichetta" data-entra>Small-batch coffee, roasted every Tuesday</p>
      <h1 class="apertura__titolo">
        <span class="riga"><span data-titolo>Pulled slow,</span></span>
        <span class="riga"><span data-titolo>poured <em>by hand.</em></span></span>
      </h1>
      <p class="apertura__testo" data-entra>Our house flat white, from the first drop to the last curl of steam. Scroll down and watch it fill.</p>
      <p class="apertura__cenno" data-entra>
        Scroll to brew
        <svg viewBox="0 0 24 32" aria-hidden="true"><path d="M12 3V27M4 19L12 28L20 19" /></svg>
      </p>
      <div class="chicchi" aria-hidden="true">
        <svg class="chicco" viewBox="0 0 60 80" data-chicco><ellipse cx="30" cy="40" rx="26" ry="36" class="chicco__corpo" /><path d="M30 6C14 26 46 52 30 74" class="chicco__solco" /></svg>
        <svg class="chicco" viewBox="0 0 60 80" data-chicco><ellipse cx="30" cy="40" rx="26" ry="36" class="chicco__corpo" /><path d="M30 6C14 26 46 52 30 74" class="chicco__solco" /></svg>
        <svg class="chicco" viewBox="0 0 60 80" data-chicco><ellipse cx="30" cy="40" rx="26" ry="36" class="chicco__corpo" /><path d="M30 6C14 26 46 52 30 74" class="chicco__solco" /></svg>
      </div>
    </section>

    <!-- la tazza: il palco resta fermo e, scorrendo, si riempie -->
    <section class="storia" id="tazza" data-storia>
      <div class="palco">
        <div class="racconto">
          <nav class="ricetta" aria-label="Steps">
            <button type="button" data-voce>Shot</button>
            <button type="button" data-voce>Crema</button>
            <button type="button" data-voce>Pour</button>
            <button type="button" data-voce>Leaf</button>
            <button type="button" data-voce>Steam</button>
          </nav>

          <div class="passi">
            <article class="passo" data-passo>
              <p class="passo__etichetta" data-linea>The shot</p>
              <h2 class="passo__titolo">
                <span class="riga"><span data-riga>Eighteen grams,</span></span>
                <span class="riga"><span data-riga>a thin <em>thread.</em></span></span>
              </h2>
              <p class="passo__testo" data-linea>Slow and steady from both spouts. If it gushes the grind is too coarse, if it drips it is too fine.</p>
            </article>
            <article class="passo" data-passo>
              <p class="passo__etichetta" data-linea>The crema</p>
              <h2 class="passo__titolo">
                <span class="riga"><span data-riga>Tiger stripes</span></span>
                <span class="riga"><span data-riga>on <em>top.</em></span></span>
              </h2>
              <p class="passo__testo" data-linea>The oils rise into a hazelnut foam. The last drops fall and the machine goes quiet.</p>
            </article>
            <article class="passo" data-passo>
              <p class="passo__etichetta" data-linea>The pour</p>
              <h2 class="passo__titolo">
                <span class="riga"><span data-riga>Milk under,</span></span>
                <span class="riga"><span data-riga>not <em>over.</em></span></span>
              </h2>
              <p class="passo__testo" data-linea>Start high, so the milk dives beneath the crema and lifts the colour from below.</p>
            </article>
            <article class="passo" data-passo>
              <p class="passo__etichetta" data-linea>The leaf</p>
              <h2 class="passo__titolo">
                <span class="riga"><span data-riga>Rock the jug,</span></span>
                <span class="riga"><span data-riga>pull it <em>through.</em></span></span>
              </h2>
              <p class="passo__testo" data-linea>Drop close, rock side to side as you drift back, then lift and draw one line down the middle. A rosetta.</p>
            </article>
            <article class="passo" data-passo>
              <p class="passo__etichetta" data-linea>The steam</p>
              <h2 class="passo__titolo">
                <span class="riga"><span data-riga>Drink it while</span></span>
                <span class="riga"><span data-riga>it <em>curls.</em></span></span>
              </h2>
              <p class="passo__testo" data-linea>Warm enough to taste the sweetness, never scalding. Scroll back up and the cup empties again.</p>
            </article>
          </div>

          <p class="misura">In the cup <strong><span data-ml>0</span> ml</strong></p>
        </div>

        <svg class="scena" data-scena viewBox="0 0 600 760" preserveAspectRatio="xMidYMax meet" aria-hidden="true">
          <defs>
            <clipPath id="bocca"><ellipse cx="300" cy="440" rx="145" ry="57" /></clipPath>
            <linearGradient id="grad-corpo" x1="0" y1="0" x2="1" y2="0">
              <stop offset="0" class="stop-tazza-ombra" />
              <stop offset="0.38" class="stop-tazza" />
              <stop offset="0.62" class="stop-tazza" />
              <stop offset="1" class="stop-tazza-ombra" />
            </linearGradient>
            <linearGradient id="grad-interno" x1="0" y1="0" x2="0" y2="1">
              <stop offset="0" class="stop-tazza-ombra" />
              <stop offset="1" class="stop-tazza" />
            </linearGradient>
            <linearGradient id="grad-piattino" x1="0" y1="0" x2="1" y2="0">
              <stop offset="0" class="stop-tazza-ombra" />
              <stop offset="0.45" class="stop-tazza" />
              <stop offset="1" class="stop-tazza-ombra" />
            </linearGradient>
            <linearGradient id="grad-acciaio" x1="0" y1="0" x2="1" y2="0">
              <stop offset="0" class="stop-acciaio-scuro" />
              <stop offset="0.35" class="stop-acciaio" />
              <stop offset="0.55" class="stop-acciaio-chiaro" />
              <stop offset="1" class="stop-acciaio-scuro" />
            </linearGradient>
            <filter id="sfuma" x="-50%" y="-20%" width="200%" height="140%"><feGaussianBlur stdDeviation="2.6" /></filter>
            <linearGradient id="grad-macchina" x1="0" y1="0" x2="0" y2="1">
              <stop offset="0" class="stop-macchina" stop-opacity="0" />
              <stop offset="0.6" class="stop-macchina" />
            </linearGradient>
            <radialGradient id="grad-crema" cx="0.5" cy="0.5" r="0.5">
              <stop offset="0" class="stop-crema-chiara" />
              <stop offset="0.7" class="stop-crema" />
              <stop offset="1" class="stop-crema-bordo" />
            </radialGradient>
            <radialGradient id="grad-misto" cx="0.5" cy="0.5" r="0.5">
              <stop offset="0" class="stop-misto" />
              <stop offset="0.75" class="stop-misto" stop-opacity="0.85" />
              <stop offset="1" class="stop-misto" stop-opacity="0" />
            </radialGradient>
          </defs>

          <!-- il piattino -->
          <g data-tazza>
            <ellipse cx="300" cy="656" rx="238" ry="92" class="piattino-ombra" />
            <ellipse cx="300" cy="646" rx="236" ry="90" fill="url(#grad-piattino)" class="piattino" />
            <ellipse cx="300" cy="640" rx="156" ry="56" class="piattino-incavo" />
            <ellipse cx="300" cy="638" rx="124" ry="38" class="ombra-tazza" />

            <!-- il manico -->
            <path d="M446 478C522 466 540 566 436 576" class="manico" />
            <path d="M446 478C522 466 540 566 436 576" class="manico-luce" />

            <!-- l'interno e la parete dietro -->
            <ellipse cx="300" cy="440" rx="150" ry="60" fill="url(#grad-interno)" />

            <!-- la superficie: tutto quello che sta dentro è disegnato visto dall'alto (raggio 100) e schiacciato -->
            <g clip-path="url(#bocca)">
              <g data-superficie>
                <circle r="100" class="espresso" />
                <circle r="100" fill="url(#grad-crema)" data-crema />
                <g data-macchie></g>
                <circle r="100" fill="url(#grad-misto)" data-misto />
                <g data-cerchi></g>
                <g data-rosetta></g>
                <path class="gambo" data-gambo />
              </g>
            </g>

            <path d="M150 440A150 60 0 0 1 450 440" class="bordo" />

            <!-- il vapore -->
            <g data-vapore filter="url(#sfuma)"></g>

            <!-- i fili che cadono -->
            <path class="filo-caffe" data-filo />
            <path class="filo-caffe" data-filo />
            <ellipse rx="2.6" ry="3.8" class="goccia" data-goccia />
            <ellipse rx="2.6" ry="3.8" class="goccia" data-goccia />
            <path class="filo-latte" data-filo-latte />

            <!-- la parete davanti -->
            <path d="M150 440C150 548 196 632 300 636C404 632 450 548 450 440A150 60 0 0 1 150 440Z" fill="url(#grad-corpo)" class="corpo" />
            <path d="M450 440A150 60 0 0 1 150 440" class="bordo bordo--davanti" />
            <path d="M174 506C184 562 210 600 246 620" class="riflesso" />
          </g>

          <!-- la macchina: il gruppo e il portafiltro con i due beccucci -->
          <g data-macchina>
            <rect x="160" y="-60" width="280" height="110" rx="22" fill="url(#grad-macchina)" />
            <rect x="160" y="18" width="280" height="10" class="macchina-riga" />
            <rect x="222" y="48" width="156" height="50" rx="9" fill="url(#grad-acciaio)" />
            <rect x="40" y="104" width="204" height="24" rx="12" transform="rotate(-5 244 116)" class="legno" />
            <path d="M228 96H372L364 134C362 142 356 146 348 146H252C244 146 238 142 236 134Z" fill="url(#grad-acciaio)" />
            <path d="M279 146H293L291 160H281Z" class="beccuccio" />
            <path d="M307 146H321L319 160H309Z" class="beccuccio" />
          </g>

          <!-- la lattiera: il becco è nell'origine, così si inclina attorno al punto da cui esce il latte -->
          <g data-brocca>
            <g data-brocca-corpo>
              <path d="M-131 30C-178 30 -180 112 -129 114" class="manico-brocca" />
              <path d="M4 0C-6 2 -11 6 -13 13L-21 132C-22 144 -31 150 -43 150H-112C-124 150 -132 144 -132 132L-130 13C-130 8 -126 4 -120 4Z" fill="url(#grad-acciaio)" class="brocca" />
              <path d="M-126 7H-12" class="brocca-bordo" />
              <path d="M-104 22L-108 136" class="brocca-luce" />
            </g>
          </g>

          <!-- dove il puntatore scuote la tazza -->
          <ellipse cx="300" cy="560" rx="220" ry="150" class="presa" data-presa />
        </svg>
      </div>
    </section>

    <footer class="fine" id="fine">
      <p class="fine__frase">Your cup is ready. <em>Come by for the real one.</em></p>
      <a class="fine__bottone" href="#tazza"><span>Order the beans</span></a>
    </footer>
  </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="https://cdn.jsdelivr.net/npm/lenis@1.3.26/dist/lenis.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.

74 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 coffee that fills as you scroll” from Animorio and fit it into my site

What it does

The page of a made-up roastery, Ashgrove Roasters, on a roasted-coffee background. At the top a big title, «Pulled slow, poured by hand.», and three beans that drop in with a bounce and then rock; the pointer makes them hop. Below, the stage stops: on the right a white cup on its saucer seen at three quarters, on the left the story in five steps, with their names at the top (Shot, Crema, Pour, Leaf, Steam) and the millilitres in the cup counting up. As you scroll, two thin threads of espresso fall from the machine’s two spouts, trembling slightly; rings spread where they land, the coffee rises dark and the crema covers it in hazelnut with its little flecks. The threads let go, two last drops fall and the machine lifts away. The milk jug slides in with a small bounce, tilts and pours from high up: the milk dives under the crema and lightens it from the middle. Then the jug drops close, rocks left and right as it drifts back, and a leaf (a rosetta) appears on the surface one layer at a time, each on its own spring, pushing the earlier ones forward; at the end the jug lifts and pulls a straight line through the middle, which sharpens the layers. The jug straightens and leaves, the leaf settles and three curls of steam rise from the cup. The text of each step leaves upwards and the next one rises from below. Scroll back and everything runs in reverse and the cup empties. The pointer on the cup rocks it on the saucer, with the coffee sloshing; the step names jump to their part.

How it works

The still stage

The section is SCROLL_LENGTH screens tall and holds a sticky stage 100lvh tall: the story on the left, on the right an SVG with a fixed viewBox (600 × 760) and preserveAspectRatio xMidYMax meet, so the cup stays sitting at the bottom at any size. The drawing is in layers: saucer, handle, inside of the cup, the coffee’s surface, back rim, steam, threads, front wall and front rim. The threads sit between the back rim and the front wall: up high they pass over the cup, at the bottom they vanish behind the wall.

A timeline of numbers

Everything that moves is a number from 0 to 1 in one object: how far the thread has fallen, how far the tail has let go, the level, the crema, the machine, the jug, the tilt, the milk, the leaf, the steam. A single paused timeline moves them, each with its own curve (back.in for the machine lifting away, back.out for the jug coming in, elastic.out for every layer of the leaf). A tween takes its time from 0 to the end with a scrubbed ScrollTrigger, from when the section touches the top to when it ends: scroll up and the timeline goes back and the cup empties.

The surface seen from above

The coffee, the crema, the milk lightening it, the rings and the leaf are drawn in a group as if seen from above, in a circle of radius 100. A GSAP ticker gives it translate, scale and rotate every frame: the centre drops and the radius shrinks when the level is low, and the vertical scale (0.4 of the radius) squashes it into the ellipse of the rim. A clipPath with the rim’s ellipse cuts off what would sit behind the wall. The threads are filled shapes rebuilt every frame: a strip that thins out, with a sine that makes it tremble, between the tail and the tip.

The leaf

Every layer of the rosetta is a crescent made of four quadratic curves: wide and tall far away, narrow close to the jug. The layers overlap, so only the edge of each one shows, thin and crema-coloured (vector-effect: non-scaling-stroke, so it doesn’t squash). A layer grows from v = 0 to 1 with elastic.out, and every open layer pushes the farther ones forward a little. Meanwhile the point where the milk lands moves from far to near, swinging on a sine, and the jug follows it. Then the middle line runs to the far side and, where it passes, drags the tip of each layer forward.

Steam, pointer and text

Each curl is a path with pathLength=1: its own endless timeline slides a 0.55-long dash along it with stroke-dashoffset and moves it sideways. The group has a blur filter and its opacity follows the steam in the timeline. The pointer on the cup takes a slosh number to ±1 and back to 0 with elastic.out: the cup turns slightly around the saucer and the surface slides and turns. The text of each step lives in the same timeline: the lines leave upwards with power3.in and come in from below with expo.out.

Values to change

NameFileDefaultWhat it changes
LEAVESscript.js7How many layers the milk leaf has.
WIGGLEscript.js1How far the jug rocks while drawing the leaf: 0 keeps it still.
SCROLL_LENGTHscript.js6How many screens of scrolling the whole cup takes: more is slower.
STEAMscript.js3How many curls of steam rise at the end.
STEAM_SPEEDscript.js1How fast the steam rises.
SLOSHscript.js1How hard the cup rocks when the pointer touches it.
CUP_MLscript.js180The millilitres of the full cup, for the counter; next to it is SHOT_ML for the espresso.
--cremastyle.css#b8743cThe colour of the crema; next to it are those of the espresso, the milk, the cup and the jug.

Try them in the Playground

When to use it

For anyone who sells or tells about something made in steps: a roastery, a café, a coffee machine or milk brand, a latte art school, a recipe to follow step by step, or any product that fills as you scroll, like a bottle or a jar.

On phones

The same animation with the sizes adapted: the story sits at the top, smaller, with the millilitre counter in small type below it, and the cup in the room left, resting on the bottom of the screen. A tap on the cup rocks it, a tap on the beans makes them hop.

With reduced motion

No pouring, no rocking, no smooth scrolling: at every step the cup and the text fade out and come back with the step already done (the espresso in the cup, the crema, the milk poured, the leaf, still steam). The opening title and beans fade in too.

Pitfalls

  • The machine at the top of the SVG ended in a straight cut, like a floating cabinet. Its body is short and fades upwards with a gradient to transparent: it seems to come in from the edge of the screen, on a phone too, where the text sits above it.
  • With a very squashed three-quarter view (0.32) the leaf turned into a set of stripes. The rim is an ellipse at 0.4 and the layers are filled overlapping crescents with a thin crema-coloured edge: it reads as a leaf, not a comb.
  • The steam curls ended in a hook, and the dash passing at the top looked like a floating question mark. The path is a long S with no hook, the dash is longer and the group has a blur filter: it reads as steam.
  • With the text lines moved by yPercent -115 on the way out, the tails of the letters (g, j, p) stayed visible above the new step. They leave by -140 and fade at the same time.
  • The jug coming in and leaving from far away was cut off sharply by the SVG’s edge. It travels a short way and fades in and out meanwhile.

Libraries

GSAP 3.15.0, Lenis 1.3.26, Fraunces (font variabile) 5.3.0, Mona Sans (font variabile) 5.3.0.

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

Testing

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