Cart Arc
An arc into the bag — clicking add-to-cart sends the product thumbnail flying along a bézier arc toward the cart, where the icon squashes like jelly and the badge number pops as it increments; rapid clicks send several in flight at once.
npx shadcn@latest add https://webberui.com/r/cart-arc.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<CartArc />
Installation
npx shadcn@latest add https://webberui.com/r/cart-arc.jsonOr, once registries are configured in components.json, install it as @webberui/cart-arc.
Usage
CartArc is both the container and the state hub; inside it you put a number of CartArcItem triggers and one CartArcCart target. Clicking a trigger throws a thumbnail along a bézier arc toward the cart, and on impact the icon squashes and the badge number pops as it increments.
import { CartArc, CartArcCart, CartArcItem } from "@/components/ui/cart-arc";
<CartArc>
<div className="flex items-center justify-between">
<span>Today's dessert</span>
<CartArcCart />
</div>
<CartArcItem image="/matcha.jpg">Add to cart</CartArcItem>
</CartArc>;To have the thumbnail take off from the product image (rather than the button), point originRef at that image element and specify what flies with flyContent:
const thumbRef = React.useRef<HTMLDivElement>(null);
<div ref={thumbRef}>
<img src="/matcha.jpg" alt="Matcha latte" />
</div>
<CartArcItem originRef={thumbRef} flyContent={<img src="/matcha.jpg" alt="" />}>
Add
</CartArcItem>;Controlled total
When count is not passed, the component manages the total itself (uncontrolled); you can set the initial value with defaultCount and observe changes with onCountChange. Passing count switches it to controlled mode, where the number is driven from the outside.
const [count, setCount] = React.useState(0);
<CartArc count={count} onCountChange={setCount}>
{/* ... */}
</CartArc>;Imperative trigger
For advanced cases, useCartArc() lets you throw a thumbnail at any moment from any viewport coordinate:
import { useCartArc } from "@/components/ui/cart-arc";
function BuyNow() {
const { addToCart, count } = useCartArc();
return (
<button
onClick={(e) => {
const r = e.currentTarget.getBoundingClientRect();
addToCart(
{ x: r.left + r.width / 2, y: r.top + r.height / 2 },
{ image: "/matcha.jpg", quantity: 2 },
);
}}
>
Buy now ({count})
</button>
);
}Props
CartArc
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | Contains CartArcItem and CartArcCart |
count | number | — | Total number of items in controlled mode |
defaultCount | number | 0 | Initial total in uncontrolled mode |
onCountChange | (count: number) => void | — | Callback when the total changes |
duration | number | 0.72 | Flight duration (seconds) |
className | string | — | Styles appended to the container |
CartArcItem
Forwards all native button attributes, and additionally provides:
| Prop | Type | Default | Description |
|---|---|---|---|
image | string | — | Image URL for the flying thumbnail |
flyContent | React.ReactNode | — | Custom content for the flying thumbnail; takes priority over image |
quantity | number | 1 | Quantity added, accumulated into the total |
originRef | React.RefObject<HTMLElement | null> | — | Reference element for the flight's starting point; when not provided the button itself is the origin |
onClick | (e) => void | — | Runs as usual after the thumbnail is thrown |
CartArcCart
Forwards all native button attributes, and additionally provides:
| Prop | Type | Default | Description |
|---|---|---|---|
icon | React.ReactNode | lucide ShoppingCart | Custom cart icon |
useCartArc()
| Returns | Type | Description |
|---|---|---|
count | number | Current total number of items in the cart |
addToCart | (origin, options?) => void | Throws a thumbnail from the viewport coordinate origin and increments the total; options supports image, content, and quantity |
How it works and accessibility
- The thumbnail flies along a quadratic bézier curve: the control point is lifted above both endpoints to form an upward arc, and along the way the thumbnail shrinks, fades out near the end, and rotates slightly
- The target coordinate is measured live with
getBoundingClientRecton every click, so it lands correctly through scrolling or layout changes - The flight layer is stacked at the very top of
document.bodywithcreatePortal, carryingaria-hiddenandpointer-events-none, so it interferes with neither the pointer nor assistive technology - Rapid clicks send several thumbnails flying at once, up to 16 concurrently; beyond that they simply count without flying, to protect performance. Every impact counts, so the total never drops one
- On impact the icon springs back through a jelly-squash keyframe with inverted scaleX/scaleY, and the badge number is replaced with a spring pop (the old number retreats downward, the new one pops up)
- The cart carries an
aria-labelstating the item count, and announces changes to the total through anaria-live="polite"region - The flight animation is cleaned up with
stop()on unmount or when it is recycled after impact, so nothing updates after unmount - When the user has "reduce motion" enabled at the system level, there is no flight, no squash, and no badge pop, but the add-to-cart counting behavior works exactly as usual
Gradient Builder
Edit gradients by dragging color stops, with an angle dial, one-click CSS copying, and a preset library.
Chromatic Variant Stage
A product display stage where picking a swatch performs three transitions at once — the background bleeds outward radially, the product image cross-dissolves, and the price and model number flip to their new values.