ui-061

Add to Cart Animation with Mini Cart

£7
or the whole Vault, £99
Open in new tab

An add to cart animation and mini cart drawer: the product photo arcs into the cart, the badge rolls, and lines, quantities and subtotal animate.

intermediate
7 more details
curved-pathodometer-countercount-updialog-animationfocus-managementkeyboard-navigationstagger

Released

About this component

The add to cart moment and the mini cart it fills, as one drop-in component. Pressing Add to cart lifts a copy of the product photo off its card and flies it along a curved path into the cart icon, shrinking and rounding as it goes. The bag squashes as it lands, and the badge count rolls up to the new figure only then, so the number never runs ahead of the photo still in the air. Several adds in quick succession each fly on their own.

Read the full effect overview

The mini cart is a side drawer with a line per product: a quantity stepper whose figure rolls in the direction it moved, a line total, and Remove, which slides the row out and collapses the space it held. The subtotal counts from the figure on screen to the new one, the empty state fades in when the last line goes, and the drawer can open itself after an add or leave the badge to do the talking. Escape, a focus trap, focus return and a scroll lock that holds on every page are built in, every change is announced through a polite live region, and reduced motion swaps the flight for a plain fade. Light and dark themes on one token list; GSAP core only.

Add to Cart Animation with Mini Cart - UI component preview

What's included

13 items
  • The product photo flies to the cart icon on a quadratic curve that lifts before it drops, shrinking and rounding into the icon
  • The cart icon squashes on impact and settles; the badge count rolls up only once the photo has landed
  • Rapid adds each fly independently, and the drawer waits for the last one before opening
  • Mini cart drawer with a quantity stepper per line, the figure rolling up or down with the change
  • Remove slides the row out and collapses its height, with focus moved to the neighbouring row
  • The subtotal counts from what is on screen to the new value, including on open after an add
  • Empty state with a Continue shopping action that closes the drawer
  • Open the drawer after each add, or keep it shut and let the badge confirm: one data attribute
  • Escape, focus trap, focus return and a scroll lock on both html and body
  • Adds, quantity changes and removals announced through a polite live region; the cart button's label carries the count
  • Reduced motion: no flight and no squash, fades in place of slides, the same end states
  • cart:add, cart:change, cart:open and cart:close events with the full line list, plus add, setQty, remove, open, close and destroy
  • Light and dark themes on one token list

Perfect for

5 use cases
  • Product listing and collection pages on a store
  • A quick-add button on product cards in a marketing site shop
  • A mini cart on a headless storefront fed by its own cart API
  • Merch and ticket shops where the cart should not navigate away
  • Prototyping a cart flow before wiring checkout

How it works

4 sections

One progress value drives the curve

The flying photo is a fixed-position copy of the card image placed over it. A single tween runs a progress value from 0 to 1 and the photo is placed on a quadratic Bezier curve at every frame, with the control point placed above both the card and the icon so the path rises before it falls. Scale and corner radius tween alongside on their own eases, so the photo shrinks late and arrives as a dot the size of the icon.

The count waits for the landing

The cart's data updates on click, so events and the live region are immediate, but the badge shows the count that has landed. The landing callback bumps the icon, a short squash then a back-eased settle on the icon rather than the button so the focus ring never distorts, and rolls the badge: the old figure slides out of a clipped one-cell grid as the new one slides in.

Rows that leave cleanly

Remove slides the row's contents sideways, then closes the row's height, padding and border down to nothing before taking it out of the DOM, so the rows below close the gap rather than jumping. Focus moves to the next row's Remove button, or to the empty state's action when the last one goes. The subtotal tweens from whatever figure is currently on screen, so a second change mid-count carries on instead of snapping.

A drawer that behaves like a dialog

The drawer is a role=dialog with aria-modal, placed off-screen by GSAP rather than a CSS transform so the second open works the same as the first. Contents land a beat after the panel. Opening locks the page on both html and body and restores the scroll offset on close; Tab is trapped, Escape and the backdrop close it, and focus returns to whatever opened it, an Add to cart button included.

Difficulty Intermediate
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Lighthouse, as measured

Google Lighthouse on this UI element's demo, from the latest scan. A measurement of the demo as shipped, not a promise for your page.

Accessibility
100
Best practices
100

No performance score, on purpose. That figure depends on how you deploy: your server's compression and caching, your CDN, the connection and the device doing the test, none of which the code controls. The same page can score very differently on two consecutive runs, so measure it where it will live.

The Vault opens this source and the whole library.

Each product gives you the HTML, CSS and JavaScript source, the full documentation and an AI setup prompt, licensed for unlimited personal and commercial projects. Every paid product is tested for desktop and mobile layouts, reduced-motion handling, animation cleanup and keyboard controls wherever there is something to operate.

One payment, over 350 products, and future additions to the library.

Documentation

What's Included

  • index.html: the demo page and the markup to copy

  • assets/style.css: component styles, plain CSS custom properties for colours

  • assets/script.js: readable, commented source with an onReady guard

  • assets/img/: four placeholder product photos (swap in your own)

  • The photo lifts off the card and travels a quadratic curve to the cart icon, shrinking and rounding into it

  • The bag squashes on impact and settles; the badge rolls up only once the photo has landed, so the figure never runs ahead

  • Rapid adds each fly on their own, and the drawer waits for the last one before it opens

  • Quantity stepper per line, the figure rolling up or down with the change, capped at data-max-qty

  • Remove slides the row out and collapses its height; focus moves to the neighbouring row

  • Subtotal counts from what is on screen to the new value, including on open after an add

  • Empty state with a Continue shopping action

  • role="dialog" drawer with aria-modal, a polite live region, and a cart button whose label carries the count

Quick Start

1. Add to your <head>:

In your downloadThe working code for this step, in index.html.

2. Add before the closing </body> tag:

In your downloadThe working code for this step, in index.html.

3. Copy the [data-cart] block from index.html into your page.

Every [data-cart] root on the page mounts itself. Everything the script reads is a data- hook, so class names are yours to rename:

Hook Where What it is
data-cart the root Holds products, cart button and drawer. Options go here (below)
data-cart-product each product With data-id, data-name, data-price (major units, 42.50) and optional data-detail
data-cart-image the product's <img> The photo that flies
data-cart-add the product's button Adds one. An inner data-cart-add-label swaps to "Added" for a moment (data-added-label to change the word)
data-cart-toggle the cart button Opens and closes the drawer
data-cart-icon the icon inside it What the photo flies into and what squashes
data-cart-count / data-cart-count-value the badge and its figure Start it hidden; the script shows it from the first item
data-cart-live a visually hidden aria-live="polite" element Announcements
data-cart-backdrop, data-cart-drawer the scrim and the dialog Both start hidden
data-cart-lines, data-cart-empty, data-cart-foot, data-cart-subtotal, data-cart-heading-count inside the drawer The list, the empty state, the footer, the subtotal figure, the "(3)" in the heading
data-cart-close any button in the drawer Closes it
template[data-cart-line-template] inside the root One line. Its own hooks: data-cart-line, data-line-inner, data-line-image, data-line-name, data-line-detail, data-line-qty-group, data-line-dec, data-line-qty, data-line-inc, data-line-total, data-line-remove

The showcase toolbar, the .stage wrapper and the "Living room" header row's text are demo furniture, not part of the component. The two demo-only rules in index.html that start the drawer below the toolbar go with it.

Options

Attribute on [data-cart] Default What it does
data-open-on-add true false keeps the drawer shut after an add and lets the badge confirm it. Read on every add, so it can be flipped at runtime
data-currency GBP Any ISO 4217 code, formatted with Intl.NumberFormat
data-locale the page's lang, else en-GB Number formatting locale
data-max-qty 9 Highest quantity per line
data-cart-manual unset true skips auto-mount; call FlyCart.mount(root) yourself

API and events

In your downloadThe working code for this step, in script.js.

cart:add (with detail.id), cart:change, cart:open and cart:close bubble from the root. The cart lives in memory; persist it from cart:change and replay it with add() on load.

Keyboard & Accessibility

Key What it does
Enter / Space on Add to cart Adds the product; with data-open-on-add, the drawer opens and focus moves to its close button
Enter / Space on the cart button Opens the drawer
Tab / Shift+Tab Cycles within the drawer while it is open; disabled steppers are skipped
Escape Closes the drawer, focus returns to whatever opened it
  • Roles: the drawer is role="dialog" with aria-modal="true" and aria-labelledby on its heading; the cart button has aria-haspopup="dialog", aria-controls and aria-expanded; each stepper is a role="group" labelled with the product name, and every stepper and Remove button carries the product name in its label.
  • Announced: "Added Pleated pendant to cart. 3 items in cart.", quantity changes with the new subtotal, removals, and "Your cart is empty". The badge is aria-hidden; the cart button's label ("Cart, 3 items") carries the count instead.
  • Remove moves focus to the next row's Remove button, the previous one, or Continue shopping when the cart empties, never to <body>.
  • Scroll lock covers <html> and <body> and restores the scroll position on close.
  • prefers-reduced-motion: no flight and no squash, the badge and stepper figures cross-fade, the drawer fades instead of sliding, rows leave at once and the subtotal is set directly. Read on every action, so a preference change applies without a reload.

How It Works

The flight. A fixed-position copy of the card image is placed over the original. One tween runs a progress value from 0 to 1 and a gsap.quickSetter writes x and y from the quadratic Bezier formula each frame, with the control point above both the card and the icon so the path rises before it falls. Scale and borderRadius tween alongside on a later-weighted ease, so the photo shrinks into a dot the size of the icon rather than crossing the page as a thumbnail. The copy is removed on complete.

Counts that wait. Data updates on click, so events and announcements are immediate, but the badge shows the count that has landed. Each number sits in a one-cell grid with overflow: hidden: the new figure enters from below (above when it went down) while the old one leaves, which is the roll. The icon squash is a two-step timeline on the SVG, not the button, so the focus ring never distorts.

Rows that leave cleanly. Remove fades the row's contents sideways, then tweens the row's measured height, padding and border to zero before removing it, so the rows below close the gap rather than jumping. The subtotal tweens from whatever figure is on screen, so a second change mid-count carries on instead of snapping back.

The drawer. GSAP places the panel with xPercent rather than a CSS transform, so the second open behaves like the first. Presence is the hidden attribute, set in onComplete, so an invisible panel never covers the page; the open fades opacity, never autoAlpha, so focus can move in on the same frame.

Customisation

  • --accent is the badge, the checkout button and the focus ring: change it first.
  • Drawer width: .cart-drawer { width: min(400px, 100%); }. On phones under 440px it is full width.
  • Flight feel: the fly() function in assets/script.js. lift is how high the arc rises, 0.66 the travel time, endScale the size it arrives at.
  • Squash: bumpIcon(). Remove back.out(2.6) for a flat settle.
  • The photo images are 560px squares; any square image works.

Themes

Two themes on one token list: light (the default) and dark.

In your downloadThe working code for this step, in index.html.

Nothing else changes: same markup, same script. The demo's toggle and the ?variant=dark URL parameter only set that attribute.

Tokens, all in the two blocks at the top of assets/style.css: --ground, --ground-2, --raised, --hover, --ink, --ink-2, --ink-3, --line, --line-strong, --accent, --accent-ink, --accent-wash, --scrim, --shadow, --shadow-panel, --shadow-fly, --font, --radius, --radius-sm, --radius-xs. Re-value them for your brand; no selector needs overriding. The body and .stage rules at the top are the demo's page and can go.

Requirements

  • GSAP 3.12+ (core only)
  • No build step, no framework

Also in your download: the full source, worked examples, the events and programmatic API, and the class reference.

£7