# Coupon input

> A React coupon code input for carts and checkouts: a link that opens it, Apply inside the box, async checks and applied codes as chips.

- Type: React component (Commerce)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add coupon-input`
- Docs: https://wingo-ui.com/components/coupon-input

## Interaction

Tap or click "Have a promo code?" and the field slides open with the cursor in it (the chevron turns). Type a code in any case with any spaces; it shows in capitals. Press Enter or Apply: with a server check the button spins while it waits, and Enter never submits the checkout form around it. A good code lands as a chip with a check that pops, the code, its label and what it saves; at the code limit the field folds away until you remove a chip. A bad code shakes the box and says what to do ("Add $45.20 more to use SAVE50", "This code expired on August 31, 2026"). Escape clears the typed code; a second Escape on an empty field closes it and puts focus back on the link. Escape while a code is being checked does nothing. Tab reaches the remove button of each chip; Enter, Space, Backspace or Delete removes it. When the cart changes, catalogue codes recount what they save and a code that no longer applies turns amber with a note under it. Every outcome is read out once by screen readers; on a phone the outcome also buzzes. Under reduced motion everything fades instead of moving.

## Installation

```bash
npx wingo-ui@latest add coupon-input
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/coupon-input` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/coupon-input.tsx`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [Spinner](https://wingo-ui.com/components/spinner) ([Markdown](https://wingo-ui.com/components/spinner.md))
- [Tag](https://wingo-ui.com/components/tag) ([Markdown](https://wingo-ui.com/components/tag.md))
- [Commerce](https://wingo-ui.com/components/commerce) ([Markdown](https://wingo-ui.com/components/commerce.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [Haptics](https://wingo-ui.com/components/haptics) ([Markdown](https://wingo-ui.com/components/haptics.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { CouponInput, type AppliedCoupon } from "@/components/ui/coupon-input"
import type { Coupon } from "@/lib/commerce"
import { useState } from "react"

const COUPONS: Coupon[] = [
  { code: "FALL15", type: "percent", value: 15, maxDiscount: 150, label: "Fall sale" },
  { code: "FREESHIP", type: "shipping", value: 0, label: "Free delivery" },
  { code: "SAVE50", type: "fixed", value: 50, minSubtotal: 300 },
]

export function CartCoupon() {
  const [applied, setApplied] = useState<AppliedCoupon[]>([])
  return (
    <CouponInput
      value={applied}
      onValueChange={setApplied}
      coupons={COUPONS}
      subtotal={489.7}
      shipping={19.99}
      locale="en-US"
      labels={{
        trigger: "Got a discount code?",
        invalid: "We don't recognize that code",
        minSubtotal: (missing, code) => `Add ${missing} more to use ${code}`,
      }}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `subtotal?` | `number` | `0` | The items' subtotal, used for minimum-order checks and to work out what a percent or fixed code saves. The demo uses its $489.70 cart while this is 0. |
| `shipping?` | `number` | `0` | The delivery cost a free-delivery code saves. The demo uses $19.99 (UPS) while this is 0. |
| `currency?` | `string` | `"USD"` | ISO 4217 code for the saved amounts and the minimum-order message: "$", "€", "lei". |
| `locale?` | `string` | `""` | Intl locale for the amounts ("en-US" prints $45.20, "ro-RO" 45,20 lei). Empty uses the UIProvider locale, then the browser's. |
| `label?` | `ReactNode` | `""` | A visible label above the box, for checkout forms. Without it the input is named by the placeholder text. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | The field box at 32 / 40 / 48px (44px on touch at md), with a matching Apply button, chips and link text. |
| `variant?` | `"soft" \| "outline"` | `"soft"` | soft is the filled well that works on any surface; outline is a hairline box for white forms. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Corner shape of the box and the Apply button; scales with the size like the button's. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The color of the soft Apply button. brand suits a checkout's one orange accent. |
| `defaultOpen?` | `boolean` | `false` | Starts with the field open instead of the link. Only matters while collapsible. |
| `disabled?` | `boolean` | `false` | Dims and blocks the link, the field, Apply and the chips' remove buttons. |
| `collapsible?` | `boolean` | `true` | Starts as the "Have a promo code?" link that opens the field. Off keeps the field open (checkout pages). |
| `maxCoupons?` | `number` | `1` | How many codes can be applied together. At the limit the field hides until a chip is removed. |
| `haptics?` | `boolean` | `true` | A success or error buzz on touch devices when a check ends (lib/haptics). Never on a mouse click. |
| `value?` | `AppliedCoupon[]` |  | The applied codes, controlled. AppliedCoupon = { code: string; label?: ReactNode; amount?: number; type?: CouponType; valid?: boolean }. |
| `defaultValue?` | `AppliedCoupon[]` | `"[]"` | The applied codes to start with, uncontrolled. |
| `onValueChange?` | `(value: AppliedCoupon[]) => void` |  | Called with the new list after a code is applied or removed. |
| `onApply?` | `((code: string) => CouponResult \| Promise<CouponResult>)` |  | The server check, called with the trimmed upper-case code. Resolve the exported CouponResult, { ok: true, coupon } or { ok: false, error }; a rejected promise shows its Error.message or labels.invalid. Wins over coupons. |
| `coupons?` | `Coupon[]` |  | A local catalogue (lib/commerce) checked case-insensitively with couponDiscount against subtotal when onApply is absent. Applied catalogue codes are also rechecked whenever subtotal or shipping change. |
| `open?` | `boolean` |  | Whether the field is open, controlled. Pair with onOpenChange. |
| `onOpenChange?` | `(open: boolean) => void` |  | Called when the link, Escape or reaching the code limit opens or closes the field. |
| `icon?` | `ReactNode` |  | The icon on the link and at the start of the box; a TicketPercent by default, null hides it. |
| `labels?` | `Partial<CouponInputLabels>` |  | Every string: trigger "Have a promo code?", placeholder "Promo code", apply "Apply", checking "Checking…", remove(code), applied(code), saved(amount) "-$45", freeShipping "Free delivery", invalid, minSubtotal(missing, code), duplicate, limit "One code per order", empty "Enter a code", noLongerValid(code), removed(code), list "Applied codes" (names the chip list for screen readers). The English set is exported as DEFAULT_COUPON_INPUT_LABELS. |
| `classNames?` | `Partial<Record<"trigger" \| "label" \| "box" \| "input" \| "apply" \| "error" \| "list" \| "chip" \| "warning", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Extra classes for the root, merged last. |

## Parts

### DEFAULT_COUPON_INPUT_LABELS (constant)

The English strings: trigger ("Have a promo code?"), placeholder, apply, checking, remove(code), applied(code), saved(amount), freeShipping, the error messages (invalid, minSubtotal, duplicate, limit, empty) and noLongerValid(code). Spread it into labels to translate a few. Type: CouponInputLabels.

### AppliedCoupon (type)

A code that is applied: { code, label?, amount?, type?, valid? }. amount is what it takes off in major units; valid: false marks a code that no longer applies.

### CouponResult (type)

What onApply returns or resolves to: { ok: true, coupon } or { ok: false, error }, where error is the message shown under the box.

## Examples

- **Cart footer**: Collapsed to a link in a narrow cart column; opens on tap, checks a local catalogue.
- **Checkout**: Always open with a visible label, an outline box and a brand Apply, checked by a server.
- **Two codes**: maxCoupons={2}: a percent code and free delivery together; the field folds away at the limit.
- **Minimum order**: SAVE50 needs $300: the error says what is missing; add a second case and it applies.
- **Server errors**: onApply rejects expired and first-order codes; the box shakes and the message says why.
- **No longer valid**: Remove the laptop stand after SAVE50 and the chip warns; add it back and it applies again.
- **Sizes**: sm, md and lg boxes with their Apply buttons and chips.

## See also

- [Cart drawer](https://wingo-ui.com/components/cart-drawer) ([Markdown](https://wingo-ui.com/components/cart-drawer.md))
- [Commerce](https://wingo-ui.com/components/commerce) ([Markdown](https://wingo-ui.com/components/commerce.md))
- [Price](https://wingo-ui.com/components/price) ([Markdown](https://wingo-ui.com/components/price.md))
- [Tag](https://wingo-ui.com/components/tag) ([Markdown](https://wingo-ui.com/components/tag.md))
- [Input](https://wingo-ui.com/components/input) ([Markdown](https://wingo-ui.com/components/input.md))

---

Canonical: https://wingo-ui.com/components/coupon-input
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
