A canvas-based scratch-to-reveal card with built-in reward variants, scoped confetti, and an imperative handle for programmatic control — ideal for promotions, giveaways, and reward flows.
Canvas overlay is erased with destination-out compositing as the user drags a pointer across it — works with mouse, touch, and stylus.
Five built-in reward variants: default, gold, success, brand, and mystery — pass any ReactNode for fully custom content.
Configurable threshold (0–1) triggers an auto-complete fade once enough of the overlay is cleared.
Scoped confetti engine renders on a canvas layered between the reward and the scratch overlay, so particles appear to burst out from behind the card on reveal.
ScratchCardHandle exposes reveal() and reset() for programmatic control via ref.
Overlay has a subtle coin-texture dot pattern and diagonal shimmer stripe for a tactile, physical feel.
pointer-events are disabled on the overlay once revealed, so the reward content becomes fully interactive.
Keyboard-accessible "Reveal" button appears over the card for non-pointer users.
Give the component explicit dimensions via className (e.g. h-40 w-72). The canvas sizes itself to its container using ResizeObserver, so fluid widths work too.
A resize clears scratch progress by design — preserving a partially-scratched bitmap across a dimension change is not meaningful, so the overlay redraws fresh.
The confetti canvas sits between the reward content and the scratch overlay in the stacking order, so particles appear to emerge from behind the card as the overlay fades out.
Progress sampling reads every 8th pixel's alpha channel for performance. On very small cards this may cause the threshold check to be slightly imprecise; lower threshold slightly if auto-complete feels sluggish.
A window blur event is not used here — the component tracks pointer state locally via setPointerCapture / releasePointerCapture, so there are no stuck-stroke issues when the user drags outside the card.
For a lottery or game flow, combine onComplete with your own prize-determination logic on the server side. Never store the prize value only in the client — use onComplete to call an API that returns the authoritative result.