Coupon Code Input
An ecommerce coupon input React component: on success the field turns into a perforated ticket chip with a tear-off stub animation and a rolling discount amount; on failure it shakes and shows the reason. The applied chip is removable and input is case-normalised.
The coupon field every checkout page needs. Input is auto-uppercased, whitespace-stripped and truncated to the length limit; pressing Apply or Enter hands the code to the onApply validator you provide, while the button shows a loading spinner and sets aria-busy. On success the input row switches to a ticket chip with a perforated left edge — a stub tears off and falls away, and the discount amount rolls in digit by digit; press the × on the right to remove it and type again. On failure the input shakes horizontally, turns red and shows the reason, ready for another try. Both the input value and the applied state support controlled and uncontrolled modes.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add coupon-code-input.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<CouponCodeInput/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add https://webberui.com/r/coupon-code-input.en.jsonOr, once registries are configured in components.json, install it as @webberui/coupon-code-input.
Usage
import { CouponCodeInput } from "@/components/ui/coupon-code-input";
<CouponCodeInput
label="Discount Code"
currency="NT$"
onApply={async (code) => {
// Validate against your API; return ok plus the discount info
if (code === "WELCOME100") {
return { ok: true, discount: 100, label: "New Customer Gift" };
}
return { ok: false, reason: "No matchingDiscount Code" };
}}
onRemove={(code) => console.log(`removed ${code}`)}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Controlled input value; when omitted the component manages it internally (uncontrolled) |
defaultValue | string | "" | Initial input value in uncontrolled mode |
onChange | (value: string) => void | — | Callback when the input changes; the argument is the normalised (uppercased, whitespace-stripped) string |
onApply | (code: string) => Promise<CouponApplyResult> | CouponApplyResult | — | Validator invoked on Apply or Enter; when omitted every code is treated as a success |
onRemove | (code: string) => void | — | Callback when the applied coupon is removed; the argument is the removed code |
applied | AppliedCoupon | null | — | Controlled applied state (null means none); when omitted the component manages it internally |
currency | string | "NT$" | Currency prefix shown before the discount amount |
placeholder | string | "Enter Discount Code" | Input placeholder text (default reads "enter a coupon code") |
label | string | "Discount Code" | Field label text (default reads "coupon code"); pass an empty string to hide it |
maxLength | number | 20 | Maximum coupon length; longer input is truncated |
disabled | boolean | false | Disable the input and buttons |
className | string | — | Forwarded to the outermost container |
AppliedCoupon
| Field | Type | Default | Description |
|---|---|---|---|
code | string | — | The applied coupon code (normalised) |
discount | number | — | Discount amount (positive); the amount and its rolling animation only show when provided |
label | string | — | Extra descriptive text (for example "new customer gift") |
CouponApplyResult
| Field | Type | Default | Description |
|---|---|---|---|
ok | boolean | — | Whether the coupon applied successfully |
discount | number | — | Discount amount on success |
label | string | — | Extra descriptive text on success |
reason | string | — | Failure reason, shown in the error message and read out by screen readers |
normalizeCouponCode(raw, maxLength) is also a named export, so the same normalisation logic can be reused at the form layer.
How it works
- Ticket visuals: the serrated perforation along the left edge is a CSS
mask(a repeating radial-gradient punching out half circles), so the notches are genuinely transparent and work on any background; after a successful apply, a small stub piece covers the right end of the ticket, then tears away, rotating and falling as it fades. - Case normalisation: every input path (typing, pasting, controlled values) goes through
normalizeCouponCode— uppercase, strip all whitespace, truncate tomaxLength. - Enter submission is IME-safe: pressing Enter while composing with a Chinese (or other) IME does not submit, and the Safari case where keydown fires only after
compositionendis handled too. - Validation is asynchronous: pressing Apply again while checking is ignored; an exception thrown by
onApplycounts as a failure with a generic error message; and if the user removes and retries while a check is in flight, the stale earlier result is discarded instead of overwriting the new state.
Accessibility
- The
labelis associated with the input viahtmlFor; on failurearia-invalidis set andaria-describedbypoints at the message area - Status messages (checking / failure reason / applied) live in a
role="status",aria-live="polite"region, so changes are announced by screen readers - The Apply button sets
aria-busyand is disabled while checking, preventing duplicate submissions; the remove button carries anaria-labelthat includes the coupon code - The applied ticket chip is a
role="group"whosearia-labelfully describes the code, discount amount and label; the per-digit rolling amount animation is hidden from assistive technology - When the user has reduce motion enabled at the system level, the tear-off, rolling amount and shake animations are disabled and the state switches directly
Group Buy Progress
A head-count group-buy card for React: stacked participant avatars, tiered group prices, a deadline countdown, a CTA that flips to "group formed" with a card pulse and confetti the moment the threshold is hit, and an automatic refund note if the deadline passes short of the minimum.
Receipt Printer
A receipt that prints after checkout like a thermal printer feeding paper line by line: processing, printing and complete stages, a serrated paper edge, tear-off and replay.