Two libraries share the card. GSAP drives the interactive parts, a quickTo tilt that follows your cursor and a staggered reveal that cascades the rows in when a card reaches the front, while framer-motion handles the mount, the advance, and the Undo bar. The dust itself is real, not a stock effect. At the moment you check the box I rasterize the card DOM to a canvas, read the pixels, and sample them into a grid of small colored squares. Each square becomes a particle with the color it sat on plus its own velocity, gravity, and lifetime, and they animate on one oversized canvas so they can blow clear of the card without clipping. The real card is hidden the instant the snapshot exists:
// Snapshot the card, then sample it into particles.
const snap = await toCanvas(cardNode, { pixelRatio: dpr })
const px = snap.getContext("2d").getImageData(0, 0, w, h).data
for (let y = 0; y < rows; y++)
for (let x = 0; x < cols; x++) {
const i = (sy * w + sx) * 4
if (px[i + 3] < 36) continue // skip transparent cells
particles.push({
x, y, r: px[i], g: px[i + 1], b: px[i + 2],
vx: 0.7 + nx * 2.4, vy: -1.4 - Math.random() * 2.6,
})
}
The motion is all decorative, so the whole thing has to work with it switched off. Under prefers-reduced-motion the tilt, the cascade, and the canvas all drop out, the card just fades, and if the rasterize ever fails it takes the same quiet path. The completion is announced in a live region rather than through the animation, so a screen reader hears "Task completed" whether the dust runs or not.