Skip to content
animorio
IT
All components

The florist's cart

Carts, Physics, SVG, TransitionsGSAPAdvancedCompressed weight: Demo 16 KBGSAP 28 KBFreeDownload the zip

A florist's cart where the order is the bouquet: every stem in the cart is a stem in the drawing, on its own spring. With plus a stem comes in from above and the others open up to make room, swinging; with minus one is pulled out; a line removed pulls out all its stems (with Undo, which puts each one back in its place) and, once delivery is free, kraft paper wraps the bouquet and a ribbon is tied into a bow.

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 florist's cart</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/young-serif@5.3.0/index.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/wix-madefor-text@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <!-- the drawings: flower heads, leaves; the bouquet and the thumbnails use them -->
  <svg class="defs" aria-hidden="true" focusable="false">
    <defs>
      <radialGradient id="g-ranunculus" cx="0.45" cy="0.35" r="0.75">
        <stop offset="0" class="st-ra-hi"/>
        <stop offset="1" class="st-ra"/>
      </radialGradient>
      <linearGradient id="g-tulip" x1="0" y1="0" x2="0" y2="1">
        <stop offset="0" class="st-tu-hi"/>
        <stop offset="1" class="st-tu"/>
      </linearGradient>
      <radialGradient id="g-anemone" cx="0.5" cy="0.5" r="0.6">
        <stop offset="0.35" class="st-an-in"/>
        <stop offset="1" class="st-an"/>
      </radialGradient>

      <g id="h-ranunculus">
        <circle r="16" class="ra-out"/>
        <path d="M-15.6 2.4A16 16 0 0 0 15.6 2.4A17 13 0 0 1-15.6 2.4Z" class="ra-shade"/>
        <circle cy="-1.6" r="12.4" class="ra-mid"/>
        <path d="M-12 0.6A12.4 12.4 0 0 0 12 0.6A13 9.6 0 0 1-12 0.6Z" class="ra-shade"/>
        <circle cy="-3" r="8.8" class="ra-out"/>
        <path d="M-8.4 -1.6A8.8 8.8 0 0 0 8.4 -1.6A9.2 6.8 0 0 1-8.4 -1.6Z" class="ra-shade"/>
        <circle cy="-4.2" r="5.2" class="ra-mid"/>
        <circle cy="-4.8" r="2.3" class="ra-heart"/>
      </g>

      <g id="h-tulip">
        <path d="M-11.5 -3C-12.5 -13 -7 -19.5 0 -21.5C7 -19.5 12.5 -13 11.5 -3C10.5 6.5 5.5 11 0 11C-5.5 11 -10.5 6.5 -11.5 -3Z" class="tu-back"/>
        <path d="M-12.5 -6C-13.5 -15.5 -9.5 -21 -4.5 -23C-2 -15 2.5 -6 3 2.5C3 8.5 1 11.2 0 11.2C-6.5 11.2 -11.5 5 -12.5 -6Z" class="tu-side"/>
        <path d="M12.5 -6C13.5 -15.5 9.5 -21 5 -22.5C2 -15 -1.5 -6 -2 2.5C-2 8.5 0 11.2 1 11.2C6.5 11.2 11.5 5 12.5 -6Z" class="tu-side"/>
        <path d="M-6.5 -1.5C-7.2 -12 -3.2 -18.5 0 -20.8C3.2 -18.5 7.2 -12 6.5 -1.5C5.8 6 3.2 10.4 0 10.8C-3.2 10.4 -5.8 6 -6.5 -1.5Z" class="tu-front"/>
        <path d="M-2.6 -14C-3.4 -8 -3 -1 -1.6 5" class="tu-gleam"/>
      </g>

      <g id="h-anemone">
        <g transform="scale(1 0.88)">
          <path d="M0 -1C-7 -4 -9.5 -12 -6 -16.5C-3 -19.5 3 -19.5 6 -16.5C9.5 -12 7 -4 0 -1Z" class="an-petal" transform="rotate(10)"/>
          <path d="M0 -1C-7 -4 -9.5 -12 -6 -16.5C-3 -19.5 3 -19.5 6 -16.5C9.5 -12 7 -4 0 -1Z" class="an-petal" transform="rotate(130)"/>
          <path d="M0 -1C-7 -4 -9.5 -12 -6 -16.5C-3 -19.5 3 -19.5 6 -16.5C9.5 -12 7 -4 0 -1Z" class="an-petal" transform="rotate(250)"/>
          <path d="M0 -1C-7.5 -4 -10 -12.5 -6.4 -17.4C-3.2 -20.4 3.2 -20.4 6.4 -17.4C10 -12.5 7.5 -4 0 -1Z" class="an-petal" transform="rotate(70)"/>
          <path d="M0 -1C-7.5 -4 -10 -12.5 -6.4 -17.4C-3.2 -20.4 3.2 -20.4 6.4 -17.4C10 -12.5 7.5 -4 0 -1Z" class="an-petal" transform="rotate(190)"/>
          <path d="M0 -1C-7.5 -4 -10 -12.5 -6.4 -17.4C-3.2 -20.4 3.2 -20.4 6.4 -17.4C10 -12.5 7.5 -4 0 -1Z" class="an-petal" transform="rotate(310)"/>
          <circle r="6.6" class="an-ring"/>
          <circle r="3.9" class="an-heart"/>
          <circle cx="-1.2" cy="-1.3" r="1.3" class="an-gleam"/>
        </g>
      </g>

      <g id="lf-eucalyptus">
        <ellipse rx="6" ry="5.2" class="eu-leaf"/>
        <path d="M-3.8 -1.2C-1.6 -3 1.8 -3 3.6 -1.4" class="eu-bloom"/>
      </g>

      <path id="lf-tulip" d="M0 0C5.5 -9 7.5 -27 2.6 -44C-2.6 -28 -4.4 -11 0 0Z" class="tu-leaf"/>
    </defs>
  </svg>

  <main class="stage">
    <!-- a florist's cart, and the order is the bouquet: every stem in it is a stem in the drawing, on its own spring.
         More stems open the bouquet, a line removed is pulled out of it (with undo), and from the free delivery
         total the bouquet is wrapped in kraft paper and tied with a ribbon -->
    <section class="cart" aria-labelledby="cart-title" data-cart data-autoclic>
      <header class="cart__head">
        <h2 class="cart__title" id="cart-title">Your bouquet</h2>
        <p class="cart__count" data-count></p>
      </header>

      <div class="table" data-table>
        <svg class="bouquet" viewBox="0 0 360 178" aria-hidden="true" data-bouquet>
          <g data-paper-back>
            <path class="paper-back" d="M166 170L86 52C96 40 108 36 120 42C130 26 148 24 160 34C172 18 190 18 200 32C212 22 230 26 238 42C250 36 264 40 274 52L194 170Z"/>
            <path class="paper-back-fold" d="M120 42C126 60 132 80 142 104M200 32C198 56 194 82 188 106M238 42C232 62 224 84 214 106"/>
          </g>
          <g data-stems-back></g>
          <g data-stems-front></g>
          <g class="twine-wrap">
            <g class="twine" data-twine>
              <path d="M171 134C176 137 184 137 189 134M170.5 138C176 141 184 141 189.5 138"/>
              <path d="M189 136C194 139 196 145 194 150"/>
            </g>
          </g>
          <g data-paper-left>
            <path class="paper-front" d="M186 172L92 78C112 86 140 92 168 90L192 150Z"/>
            <path class="paper-lip" d="M92 78C112 86 140 92 168 90L166 97C140 98 114 92 92 78Z"/>
          </g>
          <g data-paper-right>
            <path class="paper-front paper-front--r" d="M174 172L268 78C248 86 220 92 192 90L168 150Z"/>
            <path class="paper-lip" d="M268 78C248 86 220 92 192 90L194 97C220 98 246 92 268 78Z"/>
          </g>
          <g data-ribbon>
            <path class="ribbon" d="M152 130C170 135 190 135 208 130L207 138C190 143 170 143 153 138Z" data-band/>
            <g data-tails>
              <path class="ribbon ribbon--dark" d="M178 136C174 146 168 156 160 166L166 166L168 172C176 160 181 148 182 137Z" data-tail-l/>
              <path class="ribbon" d="M182 136C186 146 192 156 200 166L194 166L192 172C184 160 179 148 178 137Z" data-tail-r/>
            </g>
            <g data-loops>
              <path class="ribbon" d="M178 133C170 122 154 120 152 128C150 136 166 140 178 136Z"/>
              <path class="ribbon" d="M182 133C190 122 206 120 208 128C210 136 194 140 182 136Z"/>
              <path class="ribbon-fold" d="M176 132C168 126 160 125 157 128M184 132C192 126 200 125 203 128"/>
              <rect class="ribbon ribbon--knot" x="175.5" y="129.5" width="9" height="8.5" rx="3"/>
            </g>
          </g>
        </svg>
        <p class="goal" data-goal></p>
        <button class="restart" type="button" hidden data-restart>Start with the house mix</button>
      </div>

      <ul class="lines" aria-label="Stems in your bouquet" data-lines></ul>

      <div class="sums">
        <p class="sums__row"><span>Subtotal</span><span class="sums__n" data-sub></span></p>
        <p class="sums__row"><span>Delivery</span><span class="sums__n"><s class="sums__was" data-was></s><span data-ship></span></span></p>
      </div>

      <button class="pay" type="button" data-pay>
        <span class="pay__face" data-pay-face>
          <svg class="pay__spin" viewBox="0 0 20 20" aria-hidden="true" data-pay-spin><path d="M10 2.6a7.4 7.4 0 1 1-7.4 7.4"/></svg>
          <svg class="pay__tick" viewBox="0 0 20 20" aria-hidden="true" data-pay-tick><path d="M4.6 10.4l3.6 3.6 7.2-7.6"/></svg>
          <span class="pay__label" data-pay-label></span>
        </span>
      </button>

      <p class="sr" 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.

48 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 florist's cart” from Animorio and fit it into my site

What it does

On a white background a white cart 390 pixels wide: at the top Your bouquet with the number of stems, under it a light grey table with the bouquet drawn on it and, at the bottom left, what is left to free delivery (€2.40 away from free delivery); then four lines (Ranunculus, Tulip, Anemone, Eucalyptus), each with its little drawing, the price of a stem, Remove, the line's total and the minus and plus buttons; at the bottom Subtotal, Delivery and the Pay button with the figure. The bouquet is made of the stems in the cart, one by one: four apricot ranunculus, three raspberry tulips, two white anemones with a dark heart and three sprigs of eucalyptus with round leaves, tied halfway by a piece of string. When the page opens the stems come in one after the other from above, from left to right, and the bouquet comes together. With plus a new stem comes down from above along its own direction, goes through the binding and stops with its own bounce, while the others open up to make room and swing past their new place before they settle, their stems bending the way they go; the more stems, the wider the bouquet and the smaller the heads. With minus the line's last stem is pulled out upwards and the bouquet closes again. Remove pulls out all the line's stems, one after the other, and the line becomes a lower strip with Tulip removed and Undo: round Undo's arrow a ring wears away in the five seconds in which you can go back (it stops while the pointer or the focus is on the strip). Undo puts the same stems back in the same places; when the time is up the strip folds away and the lines below move up. Figures that change go soft and come back sharp without moving, the quantity takes a breath (bigger and heavier for a moment). At €35 the line becomes Free delivery, wrapped and tied in green, the delivery is struck through and becomes Free, and the bouquet is wrapped: the string goes, a sheet of kraft paper with a wavy edge rises behind, two sheets close in front from both sides with a little bounce and squeeze the stems a little, a burgundy ribbon goes round the binding, the whole bouquet sinks into the paper for a moment and comes back up, and the bow springs open with two swinging tails. Going back under the figure the bow comes undone, the sheets fold down into their point and the string is back. Resting the mouse on a line for a moment (or the keyboard focus) lifts its stems a little and makes the others see-through, until one of its buttons is pressed; moving the pointer through the flowers, the flowers it touches sway. Pay shows Processing with a spinner, then Order placed in green with a drawn tick, while the bouquet gives a shake as if the courier had picked it up, and after a while Pay is back. With every line removed, Start with the house mix is left, which brings back the bouquet the page opened with.

How it works

One stem, one spring

Every stem has an angle and a length that chase their place with a spring (SPRING: stiffness and damping), worked out in two half steps a frame with the step held to 1/30 of a second. A new stem starts 72 units higher and comes down into its place; a stem pulled out aims 96 units further up and fades meanwhile. A single requestAnimationFrame moves every stem and stops when none is moving any more: at rest nothing is drawn.

A place in the bouquet

Every stem is born with a preference between left and right taken in golden-ratio steps, so the kinds stay mixed (the eucalyptus goes towards the edges, like the greenery round the flowers). The stems in order of preference share the fan, up to SPREAD degrees wide and wider the more there are; the middle ones are longer, so the heads make a dome, and one row stands in front of the other. Wrapped, the fan closes by 13%.

The stem redrawn

Every frame the stem is an SVG path: straight under the binding, then a quadratic curve up to the head. The control point sits at a tighter angle, so the stem rises like a fountain and opens out towards the head, and it moves with the turning speed: the middle of the stem goes ahead and the head trails, as on a real flower. Pushed up, the part under the binding gets shorter. The head is a use turned along the curve's tangent, and the eucalyptus leaves are placed along the curve with the Bézier's points and tangents.

Removing, with Undo

Remove pulls the stems out one after the other and keeps their properties (preference, row, length, tilt): Undo makes them again, the same and in the same drawing order, so they go back to their place and their depth. The line changes height measured before and after swapping the body for the strip; Undo's ring is a circle whose stroke-dashoffset runs for UNDO seconds together with a GSAP delayedCall, and both stop with the pointer or the focus on the strip.

The paper and the bow

Free delivery starts a timeline on the paper's SVG groups, with svgOrigin at the cone's point: the sheet behind grows from the point, the two in front turn and stretch with back.out, the ribbon spreads, a push down on all the springs sinks the bouquet into the paper, and the bow and the tails arrive with elastic.out. Going back under, the sheets fold down into the point instead of opening sideways, so they never go over the line beside them. A click from a script (the library's preview) is a pretend customer: one more ranunculus, the tulips removed, Undo, one more eucalyptus and one less, one ranunculus less.

Values to change

NameFileDefaultWhat it changes
ITEMSscript.js[['Ranunculus', 3.2, 4, 'ranunculus'], …]The cart's lines: name, price of a stem in euros, how many and drawing (ranunculus, tulip, anemone or eucalyptus).
FREEscript.js35The subtotal from which delivery is free and the bouquet is wrapped, in euros.
DELIVERYscript.js4.95What delivery costs below that figure, in euros.
MAXscript.js9The most stems of one kind.
SPRINGscript.js[230, 0.42]The stems' springs: how stiff they are and how much they are held back (1 = no swing).
SPREADscript.js62How far the bouquet opens at most, in degrees either side of upright.
WRAPscript.js0.7The seconds the paper takes to wrap the bouquet.
UNDOscript.js5The seconds Undo stays after a line is removed.
BRUSHscript.js1How much the flowers sway when the pointer brushes through them (0 = not at all).

Try them in the Playground

When to use it

For the cart of a florist, a plant nursery or anyone who puts something together piece by piece (a gift hamper, a box of sweets, a crate of fruit): a list with a total, a threshold to reach and a live preview of what is being bought.

On phones

At 390 pixels the cart is 358 wide and the bouquet is redrawn at the same scale; every button takes a finger and does not wait for a double tap (touch-action manipulation). A finger moving over the bouquet sways it like the mouse, and the page still scrolls vertically (touch-action pan-y). Stems lifted and others see-through only with the mouse or the keyboard, never after a tap.

With reduced motion

No springs and no movement: stems appear and disappear with a fade straight in their place, the paper and the bow come and go with a fade, figures change without blurring, lines change without animating their height and the flowers do not sway under the pointer.

Pitfalls

  • The line's body has display grid, which beats the hidden attribute: once a line was removed, the body and the strip with Undo showed together. The body needs [hidden] { display: none } too.
  • Stems pulled out upwards left the drawing and went over the title (the SVG has overflow visible for the heads): the bouquet's table cuts whatever leaves its edges.
  • Opening sideways, the sheets of paper went over the delivery line: going back under the threshold they fold down into their point, and coming in they start almost closed.
  • A fromTo inside a timeline is drawn at once: the ribbon showed as a small red curve before the paper. It needs immediateRender false.
  • Stems put back by Undo, appended at the end of their group, were drawn over all the others: they are inserted in the order they were born.
  • Undo's time must stop with the pointer on the strip, but the pointer had come in before the time existed (it was on the line when Remove was pressed): every line remembers whether the pointer is on it and the time is born already stopped.
  • A line folding away and made again straight after (Start with the house mix) got the new one removed at the end of the old one's animation: the line to remove must be taken first, not read at the end.
  • To press plus or minus the mouse has to be on the line: with the highlight on at once, the bouquet was faded exactly while it changed and was wrapped. Now it comes on only after a moment's rest and goes off at the first press, until the pointer leaves the line.

Libraries

GSAP 3.15.0, Young Serif 5.3.0, Wix Madefor Text (font variabile) 5.3.0.

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

Testing

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