Componentform1.43 KB gzipZero dependencies
Coupon Field
Coupon code field with validation, success check and shake error.
Preview
Live Preview
Installation
Tokens guide →Add to your project via CLI (zero dependencies, code you own):
npx bigbullui add coupon-fieldOr install the full npm package:
npm install bigbulluiUsage
Open in StackBlitzimport { CouponField } from "@/components/ui/coupon-field";
<CouponField />Usage guidance
Use Coupon Field when a ticket stub surface needs Coupon code field with validation, success check and shake error. Keep props minimal and prefer composition with sibling stubs.
Avoid Coupon Field for long form flows or dense data grids where a dedicated layout block fits better.
Source code
coupon-field.tsxZero dependencies (only React + utils). Copy and paste directly into your project:
"use client";
import * as React from "react";
import { cn } from "./lib/utils";
export interface CouponFieldProps extends React.HTMLAttributes<HTMLDivElement> {
onApply?: (code: string) => void;
placeholder?: string;
}
/** Coupon code input: inline validation + voucher apply animation. */
export function CouponField({ onApply, placeholder = "KUPON KODU", className, ...props }: CouponFieldProps) {
const [code, setCode] = React.useState("");
const [state, setState] = React.useState<"idle" | "checking" | "valid" | "invalid">("idle");
const apply = () => {
if (code.trim().length < 4) return;
setState("checking");
setTimeout(() => {
// demo: 4+ character codes are valid
const ok = code.trim().length >= 4;
setState(ok ? "valid" : "invalid");
if (ok) onApply?.(code.trim().toUpperCase());
}, 700);
};
return (
<div className={cn("w-full max-w-sm", className)} {...props}>
<style>{`
@keyframes cfShake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } }
@keyframes cfCheck { from { stroke-dashoffset: 20; } to { stroke-dashoffset: 0; } }
`}</style>
<div className="flex gap-2">
<div className="relative min-w-0 flex-1">
<input
value={code}
onChange={(e) => { setCode(e.target.value.toUpperCase()); if (state !== "idle") setState("idle"); }}
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); apply(); } }}
placeholder={placeholder}
disabled={state === "valid"}
aria-label="Kupon kodu"
aria-invalid={state === "invalid"}
className={cn(
"w-full rounded-md border border-dashed border-border bg-background px-3 py-2 font-mono text-sm uppercase tracking-widest",
"transition-colors motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
state === "invalid" && "border-destructive animate-[cfShake_0.3s_ease-out] motion-reduce:animate-none",
state === "valid" && "border-success"
)}
/>
{state === "valid" && (
<span className="absolute right-2 top-1/2 -translate-y-1/2 text-success" aria-hidden="true">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6L9 17l-5-5" style={{ strokeDasharray: 20, animation: "cfCheck 0.3s ease-out both" }} /></svg>
</span>
)}
</div>
<button
onClick={apply}
disabled={code.trim().length < 4 || state === "checking" || state === "valid"}
className={cn(
"shrink-0 rounded-md border-2 border-dashed px-4 font-mono text-[10px] uppercase tracking-wider transition-colors motion-reduce:transition-none",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"disabled:pointer-events-none disabled:opacity-40",
state === "valid" ? "border-success text-success" : "border-border hover:border-foreground hover:text-foreground"
)}
>
{state === "checking" ? "…" : state === "valid" ? "✓ Applied" : "Apply"}
</button>
</div>
{state === "invalid" && <p className="mt-1 text-xs text-destructive">Code is invalid or expired.</p>}
{state === "valid" && <p className="mt-1 text-xs text-success">Discount applied to cart!</p>}
</div>
);
}
Props
| Prop | Type | Description |
|---|---|---|
| onApply | (code: string) => void | |
| placeholder | string |