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-field

Or install the full npm package:

npm install bigbullui
import { 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.tsx

Zero 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

PropTypeDescription
onApply(code: string) => void
placeholderstring