Components

Scratch Card

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.

Loading…

Features

  • 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.

Installation

pnpm dlx shadcn@latest add https://www.harshalvk.com/r/scratch-card.json

Usage

import { ScratchCard, ScratchCardHandle } from '@/components/scratch-card';
<ScratchCard
  reward="20% OFF"
  rewardLabel="your entire order"
  variant="gold"
  confetti
  className="h-40 w-72"
  onComplete={() => console.log('revealed!')}
/>

Imperative control

Use ref with ScratchCardHandle to reveal or reset programmatically:

import { useRef } from 'react';
import { ScratchCard, ScratchCardHandle } from '@/components/scratch-card';
 
export function RewardFlow() {
  const cardRef = useRef<ScratchCardHandle>(null);
 
  return (
    <>
      <ScratchCard
        ref={cardRef}
        reward="FREE SHIPPING"
        variant="success"
        confetti
        className="h-40 w-72"
      />
      <button onClick={() => cardRef.current?.reveal()}>Reveal now</button>
      <button onClick={() => cardRef.current?.reset()}>Play again</button>
    </>
  );
}

Custom reward content

Pass any ReactNode to reward for fully custom content inside the card:

<ScratchCard
  reward={
    <div className="flex h-full flex-col items-center justify-center gap-2 bg-gradient-to-br from-amber-400 to-yellow-300">
      <span className="text-4xl">🏆</span>
      <span className="text-2xl font-black text-amber-950">$500</span>
      <span className="text-xs font-semibold tracking-widest text-amber-800 uppercase">
        store credit
      </span>
    </div>
  }
  overlayColor="#44403c"
  overlayText="Grand prize inside"
  confetti
  className="h-48 w-80"
/>

API Reference

ScratchCard

PropTypeDefaultDescription
rewardstring | ReactNodeContent revealed under the scratch layer. A string uses the built-in styled layout; a node renders as-is.
variantdefault gold success brand mystery"default"Visual style of the built-in reward area. Ignored when reward is a ReactNode.
rewardLabelstringSecondary text shown beneath the reward string. Only used when reward is a string.
overlayColorstring"#a1a1aa"Base fill color of the scratch overlay. Any valid CSS color string.
overlayTextstring"Scratch to reveal"Label drawn on top of the overlay. Pass "" to omit.
brushSizenumber32Diameter in CSS px of the scratch eraser brush.
thresholdnumber0.6Fraction (0–1) of the overlay that must be cleared before auto-complete triggers.
confettibooleanfalseBurst scoped confetti from within the card when revealed.
onComplete() => voidCalled once when the threshold is crossed or reveal() is called imperatively.
disabledbooleanfalseDisables scratching entirely; overlay stays visible and inert.
classNamestringAdditional classes applied to the outermost container. Use this to set the card dimensions.

ScratchCardHandle

Returned by ref when forwarded to the component.

MethodSignatureDescription
reveal()() => voidInstantly completes the reveal and fires onComplete.
reset()() => voidRedraws the overlay and allows scratching again.

Notes

  • 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.