Invoice Lottery Checker (React)
A React component for Taiwan's uniform-invoice lottery: type the last 3 or all 8 digits of an invoice number and match it against the prize numbers you pass in — confetti, a rolling prize amount and a prize badge on a win, a gentle head-shake and a one-tap check-the-next-one button on a miss.
This is a WebberUI Pro component
Free during the launch campaign: sign up or sign in, then hit “Copy install command” in the preview above and it installs straight away — no payment, no credit card. The command below returns 401 while you are signed out.
Taiwan's uniform-invoice lottery draws every two months, and checking receipts is a national pastime. This component packages "period title + 8-cell digit input + result panel" into a self-contained checker: the winning numbers come in through prizes, and the built-in matchInvoice() applies the Ministry of Finance's published prize rules to decide the special prize, grand prize, the first-prize family (last 8 down to last 3 digits) and the additional sixth prize. It also supports a quick last-3-digits mode (which can only settle the sixth / additional sixth prize and prompts for the full number to confirm anything higher). On a win, confetti particles burst, the prize amount rolls up and a prize badge appears; on a miss you get a gentle head-shake and a "check the next one" button. The digit cells are OTP-style: auto-advance, paste distribution, inputMode="numeric", Backspace to step back.
npx shadcn@latest add "https://webberui.com/r/invoice-lottery-checker.json?t=<install token>"Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<InvoiceLotteryChecker />
Installation
npx shadcn@latest add "https://webberui.com/r/invoice-lottery-checker.json?t=<install token>"Or, once registries are configured in components.json, install it as @webberui/invoice-lottery-checker.
Usage
import {
InvoiceLotteryChecker,
matchInvoice,
} from "@/components/ui/invoice-lottery-checker";
// Sample prize numbers, not a real draw — feed real ones from your backend or the Ministry of Finance open data in production
const prizes = {
special: "13579246",
grand: "24680135",
first: ["11223344", "55667788", "99001122"],
additionalSixth: ["357", "802"],
};
<InvoiceLotteryChecker
prizes={prizes}
period="115 年 5–6 月"
onResult={(result, number) =>
console.log(number, result.prize, result.amount)
}
note="示範獎號,非真實開獎結果"
/>
// The matcher can be used on its own (for example to check a whole stack of receipts in a batch)
matchInvoice("10203788", prizes); // { prize: "sixth", amount: 200, matchedDigits: 3, partial: false }
matchInvoice("788", prizes); // { prize: "sixth", amount: 200, matchedDigits: 3, partial: true }Props
| Prop | Type | Default | Description |
|---|---|---|---|
prizes | InvoicePrizes | — | This period's winning numbers (special, grand, three first-prize numbers, additional sixth) |
period | string | "本期統一發票" (this period's uniform-invoice draw) | Period title, e.g. "115 年 5–6 月" (May–June of ROC year 115) |
mode | "full" | "last3" | "full" | Checking mode: full takes all 8 digits; last3 takes only the last 3 (can only settle the sixth / additional sixth prize) |
value | string | — | Controlled value (digits only); when omitted the component is uncontrolled |
defaultValue | string | "" | Initial value in uncontrolled mode |
onChange | (value: string) => void | — | Callback when the value changes; the argument is the cleaned digits-only string |
onResult | (result: InvoiceMatchResult, number: string) => void | — | Callback every time a check completes (automatic or manual) |
autoCheck | boolean | true | Check automatically once all cells are filled; when false the user presses the "對獎" (check) button or Enter |
showConfetti | boolean | true | Whether to burst confetti on a win |
showPrizes | boolean | true | Whether to list this period's winning numbers below the input |
note | string | — | Footnote text, e.g. "示範獎號,非真實開獎結果" (sample numbers, not a real draw) |
className | string | — | Forwarded to the outermost container |
InvoicePrizes
| Field | Type | Default | Description |
|---|---|---|---|
special | string | — | Special prize number (8 digits) |
grand | string | — | Grand prize number (8 digits) |
first | string[] | — | First prize numbers (8 digits × 3) |
additionalSixth | string[] | — | Additional sixth prize numbers (3 digits, any count) |
InvoiceMatchResult
| Field | Type | Default | Description |
|---|---|---|---|
prize | InvoicePrizeTier | — | Winning tier: "special", "grand", "first", "second", "third", "fourth", "fifth", "sixth", "additionalSixth"; null when nothing was won |
amount | number | — | Prize amount in NT$; 0 when nothing was won |
matchedDigits | number | — | How many trailing digits matched the winning number; 0 when nothing was won |
partial | boolean | — | Whether only the last 3 digits were compared (fewer than 8 digits given); when true, higher tiers are still unconfirmed |
matchInvoice(number, prizes), sanitizeInvoiceNumber(raw, maxLength?), INVOICE_PRIZE_LABELS (Chinese prize names), INVOICE_PRIZE_AMOUNTS (amount per tier) and the InvoicePrizeTier type are also named exports, so the same rules can be reused at the form layer or for batch checking.
How it works
- Prize rules: special prize, all 8 digits, NT$10,000,000; grand prize, all 8 digits, NT$2,000,000; any of the three first-prize numbers — all 8 digits NT$200,000, last 7 NT$40,000, last 6 NT$10,000, last 5 NT$4,000, last 4 NT$1,000, last 3 NT$200 (sixth prize); additional sixth prize, last 3 digits, NT$200. When several tiers apply at once, the highest wins
- With only 3 digits (
mode="last3"), only the sixth / additional sixth prize can be settled; the win panel switches to "末三碼相符,請輸入完整號碼確認更高獎項" (last three digits match — enter the full number to confirm a higher prize) andonResultreceivespartial: true - The result panel is shown only while the number still equals the one that was checked: as soon as the user hits Backspace it collapses; under
autoChecka given complete number is checked once, and only re-checked after being edited and completed again - The confetti is implemented in-house with small motion squares and depends on no other component; every piece's landing spot, rotation and size come from a deterministic pseudo-random function, so
Math.randomis never called during render and SSR and CSR output match - In 8-digit mode the last three cells are set slightly apart and the last three digits of each first-prize number are bolded in the list — that is what decides the sixth prize, so it can be checked at a glance
- No real winning numbers are built in: in production, feed
prizesfrom your backend or the Ministry of Finance open data; the demo and playground use fictional numbers only and label them vianote
Accessibility
- The real input is carried by a single native
input(inputMode="numeric",pattern="[0-9]*",maxLength); the visual cells are presentational only and markedaria-hidden, so keyboard behavior and the mobile numeric keypad behave exactly like an ordinary input; thelabelis bound withhtmlFor - The result lives in a
role="status"+aria-live="polite"region, so the tier, amount and hint are read out by screen readers; the input points at that region througharia-describedby - The "對獎" (check) and "再對下一張" (check the next one) buttons are
type="button"withfocus-visiblerings; Enter triggers a manual check, and Enter pressed mid-composition in a Chinese IME is ignored (isComposingplus Safari'skeyCode 229) - When the user has "reduce motion" enabled at the system level, the confetti, rolling amount, head-shake, cell pop-in and caret blink animations are disabled and the final result and text are shown directly
IME-Aware Search (React)
A React search box that never fires queries or lets Enter mis-select while a Zhuyin/Pinyin IME is composing — queries wait until compositionend (normal typing is debounced), a composing-state chip, highlighted matches, a / hotkey to focus, and a copy-paste IME handling pattern for your own inputs.
Moon Blocks Toss
Throw two crescent divination blocks with a 3D tumbling animation; the landing resolves to sheng-bei / xiao-bei / yin-bei and fires onResult for playful yes/no decisions