Skip to content
animorio
IT
All components

The latte art button

Buttons, PhysicsGSAP, CanvasAdvancedCompressed weight: Demo 12 KBGSAP 28 KBFreeDownload the zip

A coffee bar's order button, with a cup of espresso seen from above in the round end of the pill. At a click the milk pours and the heart is not drawn: it is carried. The coffee's surface is a grid of 72 × 72 cells holding the milk and the crema's tone, and every frame a flow moves them (semi-Lagrangian advection): first the milk dives under the crema, then rises in a white disc, and a last thin pour drawn through the disc drags the crema in and the milk out, making the notch and the point of the heart. A moment later a spoon stirs the heart into a spiral and the cup is espresso again.

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 latte art button</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bricolage-grotesque@5.3.0/wdth.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the latte art button: the order button of a coffee bar, with a cup of espresso seen from above in the round left
         end of the pill. A click pours the milk: it dives under the crema, then rises as a white disc, and a last thin
         pour drawn through it drags the crema in and the milk out into a heart. A moment later a spoon's stir turns the
         heart into a spiral and the cup is plain espresso again -->
    <div class="latte" data-latte>
      <div class="latte__unit">
        <button class="latte__btn" type="button" data-btn data-autoclic>
          <span class="latte__fill" aria-hidden="true"></span>
          <span class="latte__faces">
            <span class="latte__face" data-face="rest">
              <span class="latte__word" data-wide="Order a flat white" data-rest-text>Order a flat white</span>
              <span class="latte__price" data-rest-price>€3.80</span>
            </span>
            <span class="latte__face latte__face--two" data-face="busy" aria-hidden="true">
              <span class="latte__small" data-busy-small>Oat milk · to drink here</span>
              <span class="latte__big" data-busy-text>Pouring your flat white</span>
            </span>
            <span class="latte__face latte__face--two" data-face="done" aria-hidden="true">
              <span class="latte__big" data-done-text>Ready at the bar</span>
              <span class="latte__small" data-done-small>Order 42 · €3.80</span>
            </span>
          </span>
        </button>
        <!-- the cup seen from above: saucer and cup, the coffee on a canvas, and over it the rim's shine and the milk's
             stream; the pointer goes through them to the button -->
        <div class="latte__cup" aria-hidden="true" data-cup>
          <svg class="latte__china" viewBox="-40 -40 80 80" data-china></svg>
          <canvas class="latte__coffee" data-coffee></canvas>
          <svg class="latte__over" viewBox="-40 -40 80 80" data-over></svg>
        </div>
      </div>
      <p class="sr" aria-live="polite" data-live></p>
    </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.

21 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 latte art button” from Animorio and fit it into my site

What it does

On a white background an espresso-brown pill 60 pixels tall, with a thread of light along the top and a soft shadow under it; on the right Order a flat white in cream Bricolage Grotesque and the price €3.80. In the round left end, where the icon would be, a white cup seen from above on its saucer, which stands a little past the pill, with its handle at the bottom left; inside, the espresso with its golden crema, lighter in the middle, mottled with small dark flecks and with faint rings. Passing over the pill the words widen, the fill swells with a bounce and the cup turns a little on its saucer: the coffee lags behind and then follows, as liquid in a turning cup does. At a click the pill says Oat milk · to drink here and Pouring your flat white: a thread of milk falls in the middle and dives under the crema, which lightens around it; then the milk rises and spreads in a white disc that pushes the crema to the wall; last, the stream lifts and runs from top to bottom through the disc: at the top it drags the crema in and makes the notch, at the bottom it pulls the milk out and makes the point. On the cup there is a white heart with a thin halo of crema round it, and the pill says Ready at the bar · Order 42 · €3.80. A moment later a spoon stirs the coffee: the heart winds into a spiral, sinks, and the mottled crema of a fresh espresso is back. With the keyboard the focus shows a ring and turns the cup; Enter or Space order. On a phone a tap nudges the cup and orders.

How it works

The surface is a grid

The coffee is a 46-pixel canvas with two fields on a grid of 72 × 72 cells over it: how much milk there is (0 crema, 1 white foam) and the crema's tone (0 its dark flecks, 1 its golden light). The crema of a fresh espresso comes from two Perlin noises, a broad one for the mottling and a fine one for the flecks, plus a few rings and a lighter middle. Every frame the grid is coloured into a 72 × 72 ImageData and drawn enlarged and clipped to the cup's circle.

The flow is the barista's hand

The cells move along a flow written as the sum of three things: a whole turn of the coffee (speed −y·ω, x·ω), a source where the milk lands (a radial speed q / 2π · d / |d|², which spreads the milk by q square pixels a second) and a thin jet where the pour is dragged (a narrow Gaussian in the direction it moves). Against the wall the flow dies. Each cell looks back along the flow, in two half steps so curved paths stay curved, and takes the milk and crema that were there (semi-Lagrangian advection, read bilinearly).

How the heart is made

A GSAP timeline moves the flow's numbers as a hand would: for half a second the milk falls from high up and dives (the crema lightens around it and the milk stays under 32%); then the source rises to 580 square pixels a second and lays a white disc; last, the source stops and the jet runs from top to bottom through the disc in 0.42 seconds. At the start of its run it drags crema into the disc and makes the notch, at the end it drags milk out and makes the point: the latte heart comes from the barista's own gesture.

The colour of the foam

Each cell's colour mixes the crema (from its tone, darker towards the wall, where it is thinnest) and the milk, with a crisp edge between 0.3 and 0.6 of milk and a brown halo where the milk thins over the crema. Over the canvas an SVG lays the wall's shadow, the shine on the liquid and the cup's rim; the stream of milk is a small white disc with its shadow.

The spoon, the cup and the states

The spoon is a turn of the flow up to 7 radians a second while the milk fades: the heart winds into a spiral and sinks, then the crema is the one it started with. Under the pointer the cup turns on a spring and the coffee follows it late, dragged by the wall: so as not to blur the crema at every pass, the whole coffee is drawn turned instead of being carried cell by cell. The button is a real button with aria-disabled while it pours, an aria-live region that says Pouring your flat white… and then Ready at the bar.

Values to change

NameFileDefaultWhat it changes
LABELscript.jsOrder a flat whiteThe words at rest.
PRICEscript.js€3.80The price beside the words, and again underneath once it is ready.
DETAILSscript.jsOat milk · to drink hereThe small line while the milk pours: the milk chosen and where it is drunk.
POURINGscript.jsPouring your flat whiteThe words while the milk pours; the aria-live region says them too.
READYscript.jsReady at the barThe words once the heart is made.
TICKETscript.jsOrder 42The small line under them, before the price: the order's number.
POURscript.js1How slow the pour is: 0.6 brisk, 1.6 slow. The heart comes out the same.
HOLDscript.js2.6The seconds the heart stays before the spoon stirs it away.
HEARTscript.js1How much milk goes into the heart: 0.6 a small one, 1.3 a big one that nearly reaches the rim.
TURNscript.js14How far the cup turns under the pointer, in degrees.

Try them in the Playground

When to use it

For the order button of a coffee bar, a roastery, a café that takes orders by phone or a hotel's breakfast, and for the Add to cart of anyone selling coffee, plant milk or cups. The button fakes the order: in a real site the click sends the order and the milk pours while the server answers.

On phones

The pill is about 300 pixels wide and the saucer stands 5 pixels past it: at 390 pixels it fits. A tap nudges the cup on its saucer, squashes the fill and orders. The coffee's grid is the same, and the drawing stays sharp on dense screens.

With reduced motion

No pour and no turning: the heart is worked out at once, out of sight, and fades in on the cup; the spoon is a fade that brings the crema back. The cup does not turn under the pointer, the pill does not swell and the words do not widen (under the mouse the fill only lightens a little).

Pitfalls

  • The coffee that followed the turned cup was carried cell by cell like the milk: every pass of the mouse blurred the crema a little, and after a few turns the flecks were gone. A rigid turn needs no carrying: now the whole coffee is drawn turned, and only the pour and the spoon go through the grid.
  • The cells outside the circle kept their colour and small brown squares showed at the canvas's four corners (border-radius on the canvas was not enough). Now the drawing is clipped to the circle with ctx.clip() before it is enlarged.
  • A crema made from noise in polar coordinates came out swirled, as if already stirred. Now it comes from a broad and a fine noise in ordinary coordinates, plus faint rings and a lighter middle, like a freshly pulled espresso.
  • With a strong jet the heart came out long and narrow like a tulip. Now the disc is wider and the jet lighter: the notch and the point are made without stretching the disc.

Libraries

GSAP 3.15.0, Bricolage Grotesque (font variabile) 5.3.0.

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

Testing

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