Skip to content
animorio
IT
All components

The cone stepper

Counters, Physics, SVGGSAPAdvancedCompressed weight: Demo 15 KBGSAP 28 KBFreeDownload the zip

How many scoops on a gelato cone: with plus a scoop drops onto the stack, squashes as it lands and rolls to the middle of the one below, and the tower sways on its springs, slower the taller it is; with minus the top one lifts off and goes. Past the most the tower teeters and stands back up, below the fewest the last scoop will not come off the cone.

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 cone stepper</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bricolage-grotesque@5.3.0/opsz.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- how many scoops on a gelato cone: a card with the cone and, under it, a stepper. Every scoop added drops
         onto the stack and squashes, and the tower sways on its springs; pushed past the most scoops it teeters,
         and the last scoop will not come off. The scoops are drawn by the script -->
    <div class="cone-card" data-card>
      <svg class="scene" viewBox="0 0 340 300" aria-hidden="true" data-scene>
        <defs>
          <!-- one scoop, 60 wide, centred on its middle: a dome with a ruffled skirt where the scoop was turned -->
          <path id="gelato-scoop" d="M-30 7C-31.5-13-17.5-29 0-29C17.5-29 31.5-13 30 7C30.8 9.5 32 12.5 30.6 15.6Q26.2 21.5 21.9 17.2Q17.5 23 13.1 16.4Q8.7 22 4.4 17.6Q0 23.4-4.4 16.8Q-8.7 22.2-13.1 17.4Q-17.5 23-21.9 16.6Q-26.2 21.6-30.6 15.6C-32 12.5-30.8 9.5-30 7Z"/>
          <clipPath id="gelato-in-scoop"><use href="#gelato-scoop"/></clipPath>
          <radialGradient id="gelato-round" cx="0.38" cy="0.3" r="0.78">
            <stop offset="0" stop-color="#ffffff" stop-opacity="0.34"/>
            <stop offset="0.34" stop-color="#ffffff" stop-opacity="0"/>
            <stop offset="0.68" stop-color="#3c140a" stop-opacity="0"/>
            <stop offset="1" stop-color="#3c140a" stop-opacity="0.26"/>
          </radialGradient>
          <linearGradient id="gelato-ruffle" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0.7" stop-color="#3c140a" stop-opacity="0"/>
            <stop offset="0.75" stop-color="#3c140a" stop-opacity="0.09"/>
            <stop offset="1" stop-color="#3c140a" stop-opacity="0.2"/>
          </linearGradient>
          <radialGradient id="gelato-contact">
            <stop offset="0" stop-color="#3c140a" stop-opacity="0.34"/>
            <stop offset="1" stop-color="#3c140a" stop-opacity="0"/>
          </radialGradient>
          <linearGradient id="gelato-wafer" x1="0" y1="0" x2="1" y2="0">
            <stop offset="0" class="wafer-light"/>
            <stop offset="0.45" class="wafer-mid"/>
            <stop offset="1" class="wafer-dark"/>
          </linearGradient>
          <radialGradient id="gelato-floor">
            <stop offset="0" class="floor-shade" stop-opacity="0.3"/>
            <stop offset="1" class="floor-shade" stop-opacity="0"/>
          </radialGradient>
          <clipPath id="gelato-in-cone"><path d="M-27.5 3H27.5L3.4 84.5Q0 90-3.4 84.5Z"/></clipPath>
          <!-- the very top of the scene fades out, so a scoop dropped onto a tall stack comes in softly -->
          <linearGradient id="gelato-fade" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0" stop-color="#ffffff" stop-opacity="0"/>
            <stop offset="0.1" stop-color="#ffffff" stop-opacity="1"/>
          </linearGradient>
          <mask id="gelato-top" maskUnits="userSpaceOnUse" x="0" y="0" width="340" height="300">
            <rect width="340" height="300" fill="url(#gelato-fade)"/>
          </mask>
        </defs>

        <g mask="url(#gelato-top)">
          <!-- the world: the cone's rim at 0 0, the floor 100 below; the script scales it to fit the most scoops -->
          <g data-world>
            <ellipse class="floor" cx="0" cy="100" rx="30" ry="5" fill="url(#gelato-floor)" data-floor/>
            <g data-cone>
              <ellipse class="cone__mouth" cx="0" cy="-2.5" rx="28" ry="3.6"/>
              <path class="cone__body" d="M-27.5 3H27.5L3.4 84.5Q0 90-3.4 84.5Z" fill="url(#gelato-wafer)"/>
              <g class="cone__grid" clip-path="url(#gelato-in-cone)" data-grid></g>
              <path class="cone__rim" d="M-30-2.5C-20-4.8 20-4.8 30-2.5L30.4 4.5C20 8.2-20 8.2-30.4 4.5Z"/>
              <path class="cone__rim-edge" d="M-30.4 4.5C-20 8.2 20 8.2 30.4 4.5"/>
              <path class="cone__rim-light" d="M-28-2C-18-4.2 18-4.2 28-2"/>
              <ellipse class="cone__contact" cx="0" cy="3" rx="27" ry="6" fill="url(#gelato-contact)" data-cone-contact/>
            </g>
            <g data-scoops></g>
            <g data-flying></g>
          </g>
        </g>
      </svg>

      <div class="order">
        <div class="order__label">
          <span class="order__name" id="scoops-name">Scoops</span>
          <span class="order__note" id="scoops-note"><span class="order__note-in" data-note>€1.90 each</span></span>
        </div>

        <div class="stepper">
          <button class="stepper__btn" type="button" aria-label="Remove one scoop" data-less>
            <span class="stepper__disc" aria-hidden="true">
              <svg viewBox="0 0 24 24"><path d="M6.5 12h11"/></svg>
            </span>
          </button>
          <input class="stepper__value" type="text" inputmode="numeric" autocomplete="off" spellcheck="false" maxlength="2" role="spinbutton" aria-labelledby="scoops-name" aria-describedby="scoops-note" aria-valuemin="1" aria-valuemax="4" aria-valuenow="3" aria-valuetext="3 scoops" value="3" data-value>
          <button class="stepper__btn" type="button" aria-label="Add one scoop" data-more data-autoclic>
            <span class="stepper__disc" aria-hidden="true">
              <svg viewBox="0 0 24 24"><path d="M6.5 12h11M12 6.5v11"/></svg>
            </span>
          </button>
        </div>
      </div>

      <p class="sr" aria-live="polite" data-said></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.

41 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 cone stepper” from Animorio and fit it into my site

What it does

On a white background a mint green card with very round corners. At the top a wafer cone with its pressed grid, floating above its shadow, and on it three scoops: strawberry, pistachio and stracciatella, each with its ruffled skirt, the bits inside, the mark of the scoop and the shadow of the one above. When the page opens the three scoops drop from above one after the other and settle on the cone. At the bottom, on the left, Scoops and under it €1.90 each; on the right a chocolate coloured pill 60 pixels tall with minus, the number 3 and plus. With plus a chocolate scoop comes in from the top of the card, falls, squashes as it lands, rolls to the middle of the one below and the tower sways a little; the cone, held as if by a hand, gives a couple of pixels. At 4 the plus goes dim and Max 4 per cone rises under the label, in red. Pressing plus again the number does not change: the tower is shoved away from the plus, bends to one side, the scoops slip a little on each other, then it stands back up swaying; held down it stays bent until it is let go. With minus the top scoop lifts off and fades as it rises, and the stack, lighter, gives a small bounce. Below 1 the last scoop is tugged up a few pixels and drops back onto the cone, squashing, and At least 1 scoop appears under the label. Quick clicks add up: with plus a column of scoops falls and they land one on the other, and a minus while they fall sends the last one back up. The number can be typed too: a digit typed takes its place at once.

How it works

A scoop drawn once

The scoop is a single path in a box 60 wide: a dome and under it a skirt with seven lobes, the ruffled part where the scoop turned it. Every scoop reuses it with <use> and lays its flavour on it (a CSS variable), the bits scattered the same way every time by a seeded generator, two shadings (light at the top left, shade on the edge and on the skirt), the mark of the scoop and the shadow of the one above, cut by the shape itself with a clipPath in local coordinates. The cone has its wafer grid in two families of slanted lines, each with its line of light.

A chain of joints

Every scoop rests on the one below at a joint: an angle on a spring, an offset from the middle that rolls back to zero and a height that squashes (1 = round). Every frame the tower is rebuilt from the cone up: the point it rests on, its middle (height times squash), the point it offers to the scoop above. The squash is anchored at the resting point, so a squashed scoop lowers all the ones above it and the blow runs along the stack.

The weight above

Each joint is pulled sideways by the sine of the lean of every scoop above it: a tall tower is softer and slower than a short one. The load couples all the joints, and the softest way they can bend together is 1 / (4 sin²(π / (4n + 2))) times softer than a single joint (8.3 times with four scoops): the pull of the weight is kept at two thirds of that threshold, so even the tallest tower always stands back up.

The whip, with no extra energy

A joint that speeds up pushes its neighbours the other way, like the inertia of a chain: the accelerations come from solving a symmetric tridiagonal mass matrix (1 on the diagonal, WHIP beside it) with the Thomas algorithm. Symmetric means the exchange between joints never adds energy: the top lags behind the base and the whip dies out. Everything is integrated with semi-implicit Euler in steps of 1/240 of a second, with the frame step capped at 1/30.

Landing without a jump

A new scoop starts just above the card at the speed THROW and falls with GRAVITY, guided towards the top of the stack but never exactly to the middle. When its middle reaches the right height above the resting point it becomes part of the tower keeping exactly the place it had: the offset from the middle comes from the sideways distance, the angle from the lean of the stack, the squash from the vertical distance. Then it takes the blow: the squash, a share for the scoops below, the cone giving way, and a shove to the joints on the side it landed. With quick clicks every scoop starts above the one before, still in the air, and a minus sends the highest falling one back up first.

Limits, hands and keys

Past the most every joint gets a speed to the left, a little more going up, so the top leans about TEETER degrees, and the scoops slip on each other; repeated shoves add less and less and two soft stops (on every joint and on the lean of every scoop) never let it fall. Below the fewest a GSAP timeline tugs the last scoop up 9 pixels and lets it drop. A button held down repeats faster and faster (HOLD); the number is a field with role="spinbutton" and aria-valuenow, it answers the arrows, Home and End, and takes the digit typed wherever the caret was; an aria-live region says the number once the clicking is over, and always the limits.

Values to change

NameFileDefaultWhat it changes
MINscript.js1The fewest scoops (0 lets the cone go empty).
MAXscript.js4The most scoops, up to 7: above 4 the cone gets smaller to fit them.
STARTscript.js3The scoops when the page opens.
THROWscript.js600How fast a scoop comes in from the top of the card, in pixels per second.
GRAVITYscript.js2400How fast a falling scoop speeds up, in pixels per second per second.
SQUASHscript.js0.2How much a scoop squashes as it lands (0 = not at all).
SWAYscript.js[100, 0.3]The tower’s springs: how stiff they are and how much they are held back (1 = no sway).
TEETERscript.js17How many degrees the tower leans when pushed past the most.
HOLDscript.js[0.42, 0.16, 0.06]Holding a button down: the wait before it repeats, the first gap and the shortest, in seconds.
WORDscript.js['scoop', 'scoops']The word read after the number, for one and for more.

Try them in the Playground

When to use it

For choosing how many where the amount can be shown: scoops, portions, slices, tiers of a cake, books on a pile, boxes in a shipment. It suits a takeaway order or a product configurator, where the limit is real (a cone only holds so much) and is worth feeling: the teetering tower says it before the words do.

On phones

On a phone the card fits in 390 pixels with everything it has, and the buttons are 48 pixel discs. A finger works like the mouse: a tap counts at once, held down it repeats faster and faster and past the most it keeps the tower bent; a long press opens no menu and two quick taps do not zoom the page. Tapping the number opens the numeric keyboard.

With reduced motion

The scoops do not fall or squash and the tower does not sway: a scoop added appears in its place with a fade and one taken off fades where it is. Past the limits the tower stays still and only the note under the label changes, fading instead of sliding; the buttons do not shrink when pressed.

Pitfalls

  • With the weight of the scoops above, the tower fell to one side and stayed bent. The load couples all the joints, and the way they bend all together is much softer than one joint alone: 8.3 times with four scoops. The weight has to stay under the stiffness divided by that number, 1 / (4 sin²(π / (4n + 2))); here at two thirds.
  • For the whip each joint lost a little of the acceleration of the one below: a push in one direction only, which with little damping put energy in and the tower swayed for seconds. With a symmetric mass matrix, solved with the Thomas algorithm, the energy never grows.
  • Repeated shoves against the most, holding the button, added up until the tower bent over 60 degrees. Each shove now counts less the more the top is already bent, and two soft stops keep every joint within 17 degrees and every scoop within 32 of upright.
  • On the first click the new scoop, which had no position yet, was drawn for one frame on the rim of the cone, over the bottom one. Now it is born in its place, above the card, and the scene is redrawn right after the click.
  • A scoop appearing in mid air with a fade looked like it was materialising. Now it always comes in from the top of the card through a mask that fades the first 30 pixels of the scene, and with MAX above 4 the scene shrinks so the tallest stack stays below that fade.
  • Flavours given in the order the scoops were made changed the tower’s colours after taking off and putting back: every place in the stack has its flavour, so the same number always looks the same.
  • The shove against the limit used overwrite: true on the number and killed its little hop half way: the number stayed shifted and faded. With overwrite: 'auto' each tween leaves the other tweens’ properties alone.
  • Playwright, and the test run with it, will not click a button with aria-disabled: the data-autoclic mark moves to the other button as soon as a limit is pushed, and at the start if it starts at the most.
  • Typing a digit after the one already there made the field 32: only the digit just typed is read (InputEvent.data), and the field selects itself, so the next digit replaces it. A paste or a deletion is read from the whole field instead.
  • With reduced motion the two notes under the label faded at the same time and for a moment showed one over the other: now the old one fades out before the new one comes in.

Libraries

GSAP 3.15.0, Bricolage Grotesque (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 15 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