Payment Form
Original · freecard payment form with formatting
byWeb InnoventixReact + Tailwind
formpaymentforms
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/form-payment.jsonform-payment.tsx
"use client";
import { useId, useMemo, useRef, useState } from "react";
import type { ChangeEvent, FormEvent, ReactNode, RefObject } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type CardBrand = "visa" | "mastercard" | "amex" | "discover" | "unknown";
type FieldKey = "number" | "name" | "expiry" | "cvc" | "zip";
type FieldErrors = Partial<Record<FieldKey, string>>;
const AMOUNT = "228.00";
function detectBrand(digits: string): CardBrand {
if (/^4/.test(digits)) return "visa";
if (/^(5[1-5]|2(2[2-9]|[3-6]\d|7[01]|720))/.test(digits)) return "mastercard";
if (/^3[47]/.test(digits)) return "amex";
if (/^(6011|65|64[4-9])/.test(digits)) return "discover";
return "unknown";
}
function formatCardNumber(digits: string, brand: CardBrand): string {
if (brand === "amex") {
return [digits.slice(0, 4), digits.slice(4, 10), digits.slice(10, 15)]
.filter(Boolean)
.join(" ");
}
const groups: string[] = [];
for (let i = 0; i < digits.length; i += 4) groups.push(digits.slice(i, i + 4));
return groups.join(" ");
}
function formatExpiry(value: string): string {
let d = value.replace(/\D/g, "");
if (d.length === 1 && Number(d) > 1) d = "0" + d;
d = d.slice(0, 4);
if (d.length <= 2) return d;
return `${d.slice(0, 2)}/${d.slice(2)}`;
}
function luhnValid(digits: string): boolean {
if (!digits) return false;
let sum = 0;
let alt = false;
for (let i = digits.length - 1; i >= 0; i--) {
let n = Number(digits[i]);
if (alt) {
n *= 2;
if (n > 9) n -= 9;
}
sum += n;
alt = !alt;
}
return sum % 10 === 0;
}
function previewNumber(digits: string, brand: CardBrand): string {
const groups = brand === "amex" ? [4, 6, 5] : [4, 4, 4, 4];
let idx = 0;
return groups
.map((g) => {
let s = "";
for (let i = 0; i < g; i++) {
s += digits[idx] ?? "•";
idx++;
}
return s;
})
.join(" ");
}
function getErrors(f: {
number: string;
name: string;
expiry: string;
cvc: string;
zip: string;
}): FieldErrors {
const e: FieldErrors = {};
const digits = f.number.replace(/\D/g, "");
const brand = detectBrand(digits);
const expectedLen = brand === "amex" ? 15 : 16;
if (!digits) e.number = "Enter your card number.";
else if (digits.length < expectedLen) e.number = "Card number is incomplete.";
else if (!luhnValid(digits)) e.number = "This card number looks invalid.";
if (!f.name.trim()) e.name = "Enter the name on the card.";
else if (f.name.trim().length < 2) e.name = "That name is too short.";
const exp = f.expiry.replace(/\D/g, "");
if (exp.length < 4) e.expiry = "Enter a valid expiry.";
else {
const mm = Number(exp.slice(0, 2));
const yy = 2000 + Number(exp.slice(2, 4));
const now = new Date();
const curYear = now.getFullYear();
const curMonth = now.getMonth() + 1;
if (mm < 1 || mm > 12) e.expiry = "Invalid month.";
else if (yy < curYear || (yy === curYear && mm < curMonth))
e.expiry = "This card has expired.";
}
const cvcLen = brand === "amex" ? 4 : 3;
const cvcDigits = f.cvc.replace(/\D/g, "");
if (!cvcDigits) e.cvc = "Enter the security code.";
else if (cvcDigits.length < cvcLen) e.cvc = `CVC must be ${cvcLen} digits.`;
const zip = f.zip.trim();
if (!zip) e.zip = "Enter your billing ZIP.";
else if (!/^[A-Za-z0-9][A-Za-z0-9 -]{2,9}$/.test(zip)) e.zip = "That ZIP looks off.";
return e;
}
function BrandMark({ brand, className }: { brand: CardBrand; className?: string }): ReactNode {
const cls = className ?? "h-5 w-auto";
if (brand === "visa") {
return (
<span className={`inline-flex items-center font-black italic tracking-tight ${cls}`}>
VISA
</span>
);
}
if (brand === "amex") {
return (
<span className={`inline-flex items-center font-bold tracking-widest ${cls}`}>AMEX</span>
);
}
if (brand === "discover") {
return (
<span className={`inline-flex items-center gap-1 font-bold tracking-tight ${cls}`}>
DISC
<span className="inline-block h-2 w-2 rounded-full bg-amber-500" />
</span>
);
}
if (brand === "mastercard") {
return (
<svg viewBox="0 0 40 24" className={cls} role="img" aria-label="Mastercard">
<circle cx="15" cy="12" r="9" fill="#f59e0b" />
<circle cx="25" cy="12" r="9" fill="#f43f5e" fillOpacity="0.85" />
</svg>
);
}
return (
<svg viewBox="0 0 40 24" className={cls} role="img" aria-label="Card" fill="none">
<rect x="1.5" y="3.5" width="37" height="17" rx="3" stroke="currentColor" strokeWidth="1.6" />
<rect x="1.5" y="7.5" width="37" height="4" fill="currentColor" opacity="0.5" />
</svg>
);
}
const BRAND_LABEL: Record<CardBrand, string> = {
visa: "Visa",
mastercard: "Mastercard",
amex: "American Express",
discover: "Discover",
unknown: "your card",
};
export default function PaymentForm() {
const reduce = useReducedMotion();
const uid = useId();
const fid = (k: string) => `${uid}-${k}`;
const [number, setNumber] = useState("");
const [name, setName] = useState("");
const [expiry, setExpiry] = useState("");
const [cvc, setCvc] = useState("");
const [zip, setZip] = useState("");
const [touched, setTouched] = useState<Partial<Record<FieldKey, boolean>>>({});
const [cvcFocused, setCvcFocused] = useState(false);
const [status, setStatus] = useState<"idle" | "processing" | "success">("idle");
const numberRef = useRef<HTMLInputElement>(null);
const nameRef = useRef<HTMLInputElement>(null);
const expiryRef = useRef<HTMLInputElement>(null);
const cvcRef = useRef<HTMLInputElement>(null);
const zipRef = useRef<HTMLInputElement>(null);
const digits = number.replace(/\D/g, "");
const brand = detectBrand(digits);
const errors = useMemo(
() => getErrors({ number, name, expiry, cvc, zip }),
[number, name, expiry, cvc, zip],
);
const show = (k: FieldKey): string | undefined =>
touched[k] || status !== "idle" ? errors[k] : undefined;
const onNumber = (e: ChangeEvent<HTMLInputElement>) => {
const raw = e.target.value.replace(/\D/g, "");
const b = detectBrand(raw);
const max = b === "amex" ? 15 : 16;
setNumber(formatCardNumber(raw.slice(0, max), b));
};
const onName = (e: ChangeEvent<HTMLInputElement>) =>
setName(e.target.value.replace(/[^A-Za-z\s'.-]/g, "").slice(0, 26));
const onExpiry = (e: ChangeEvent<HTMLInputElement>) => setExpiry(formatExpiry(e.target.value));
const onCvc = (e: ChangeEvent<HTMLInputElement>) =>
setCvc(e.target.value.replace(/\D/g, "").slice(0, brand === "amex" ? 4 : 3));
const onZip = (e: ChangeEvent<HTMLInputElement>) =>
setZip(e.target.value.replace(/[^A-Za-z0-9 -]/g, "").slice(0, 10));
const blur = (k: FieldKey) => setTouched((t) => ({ ...t, [k]: true }));
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (status === "processing") return;
const eObj = getErrors({ number, name, expiry, cvc, zip });
setTouched({ number: true, name: true, expiry: true, cvc: true, zip: true });
if (Object.keys(eObj).length > 0) {
const order: [FieldKey, RefObject<HTMLInputElement | null>][] = [
["number", numberRef],
["name", nameRef],
["expiry", expiryRef],
["cvc", cvcRef],
["zip", zipRef],
];
for (const [key, ref] of order) {
if (eObj[key]) {
ref.current?.focus();
break;
}
}
return;
}
setStatus("processing");
window.setTimeout(() => setStatus("success"), 1500);
};
const reset = () => {
setNumber("");
setName("");
setExpiry("");
setCvc("");
setZip("");
setTouched({});
setStatus("idle");
setCvcFocused(false);
};
const inputBase =
"w-full rounded-xl border bg-white/80 px-3.5 py-3 text-[15px] font-medium text-neutral-900 placeholder:text-neutral-400 shadow-sm outline-none transition-colors duration-150 focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-offset-white disabled:opacity-60 dark:bg-neutral-900/70 dark:text-neutral-50 dark:placeholder:text-neutral-500 dark:focus-visible:ring-offset-neutral-950";
const fieldClass = (k: FieldKey): string =>
show(k)
? `${inputBase} border-rose-400 focus-visible:ring-rose-400 dark:border-rose-500/70`
: `${inputBase} border-neutral-200 focus-visible:border-indigo-500 focus-visible:ring-indigo-400 dark:border-neutral-700`;
const spring = reduce
? { duration: 0 }
: { type: "spring" as const, stiffness: 260, damping: 26 };
const swap = reduce
? { duration: 0 }
: { duration: 0.32, ease: "easeOut" as const };
const cardName = name ? name.toUpperCase() : "YOUR NAME";
return (
<section className="relative w-full overflow-hidden bg-neutral-50 px-4 py-16 text-neutral-900 sm:py-24 dark:bg-neutral-950 dark:text-neutral-50">
<style>{`
@keyframes fpay-sheen { 0% { transform: translateX(-140%) skewX(-18deg); } 60%, 100% { transform: translateX(260%) skewX(-18deg); } }
@keyframes fpay-spin { to { transform: rotate(360deg); } }
@keyframes fpay-draw { to { stroke-dashoffset: 0; } }
@keyframes fpay-pop { 0% { transform: scale(0.7); opacity: 0; } 60% { transform: scale(1.06); } 100% { transform: scale(1); opacity: 1; } }
.fpay-sheen { animation: fpay-sheen 7s ease-in-out infinite; }
.fpay-spin { animation: fpay-spin 0.7s linear infinite; }
.fpay-check { stroke-dasharray: 40; stroke-dashoffset: 40; animation: fpay-draw 0.5s 0.15s ease forwards; }
.fpay-pop { animation: fpay-pop 0.45s cubic-bezier(0.22,1,0.36,1) forwards; }
@media (prefers-reduced-motion: reduce) {
.fpay-sheen { animation: none; opacity: 0; }
.fpay-spin { animation-duration: 1.5s; }
.fpay-check { animation: none; stroke-dashoffset: 0; }
.fpay-pop { animation: none; }
}
`}</style>
{/* decorative backdrop */}
<div aria-hidden className="pointer-events-none absolute inset-0 overflow-hidden">
<div className="absolute -left-24 -top-24 h-72 w-72 rounded-full bg-indigo-300/30 blur-3xl dark:bg-indigo-600/20" />
<div className="absolute -bottom-32 -right-16 h-80 w-80 rounded-full bg-violet-300/30 blur-3xl dark:bg-violet-700/20" />
</div>
<div className="relative mx-auto max-w-5xl">
<div className="grid overflow-hidden rounded-3xl border border-neutral-200 bg-white shadow-2xl shadow-neutral-900/5 lg:grid-cols-[1fr_1.05fr] dark:border-neutral-800 dark:bg-neutral-900 dark:shadow-black/40">
{/* ---- Left: summary + live card ---- */}
<div className="relative flex flex-col gap-8 border-b border-neutral-200 bg-gradient-to-br from-neutral-50 to-neutral-100/60 p-7 sm:p-9 lg:border-b-0 lg:border-r dark:border-neutral-800 dark:from-neutral-900 dark:to-neutral-950">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-indigo-600 dark:text-indigo-400">
Order summary
</p>
<h2 className="mt-2 text-2xl font-semibold tracking-tight">Aurora Pro</h2>
<p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400">
Billed annually · renews Jul 16, 2027
</p>
</div>
{/* live card preview */}
<div
aria-hidden
className="[perspective:1400px]"
style={{ perspective: "1400px" }}
>
<motion.div
className="relative aspect-[86/54] w-full max-w-sm [transform-style:preserve-3d]"
animate={{ rotateY: cvcFocused ? 180 : 0 }}
transition={spring}
>
{/* front */}
<div className="absolute inset-0 flex flex-col justify-between overflow-hidden rounded-2xl bg-gradient-to-br from-indigo-600 via-violet-600 to-indigo-800 p-5 text-white shadow-xl shadow-indigo-900/30 [backface-visibility:hidden]">
<div className="pointer-events-none absolute inset-0 opacity-40 fpay-sheen bg-gradient-to-r from-transparent via-white/40 to-transparent" />
<div className="pointer-events-none absolute -right-10 -top-10 h-40 w-40 rounded-full bg-white/10" />
<div className="relative flex items-start justify-between">
{/* chip */}
<div className="h-9 w-12 rounded-md bg-gradient-to-br from-amber-200 to-amber-400 shadow-inner">
<div className="mx-auto mt-1.5 h-6 w-8 rounded-sm border border-amber-500/40" />
</div>
<div className="min-h-[1.25rem] text-right">
<BrandMark brand={brand} className="h-5 text-base text-white" />
</div>
</div>
<div className="relative font-mono text-lg tracking-[0.12em] tabular-nums sm:text-xl">
{previewNumber(digits, brand)}
</div>
<div className="relative flex items-end justify-between gap-3">
<div className="min-w-0">
<p className="text-[9px] uppercase tracking-widest text-white/60">
Card holder
</p>
<p className="truncate text-sm font-medium tracking-wide">{cardName}</p>
</div>
<div className="text-right">
<p className="text-[9px] uppercase tracking-widest text-white/60">Expires</p>
<p className="font-mono text-sm tabular-nums">{expiry || "MM/YY"}</p>
</div>
</div>
</div>
{/* back */}
<div className="absolute inset-0 flex flex-col overflow-hidden rounded-2xl bg-gradient-to-br from-neutral-800 to-neutral-900 text-white shadow-xl [backface-visibility:hidden] [transform:rotateY(180deg)]">
<div className="mt-5 h-10 w-full bg-black/80" />
<div className="px-5 pt-4">
<div className="flex items-center gap-3">
<div className="h-8 flex-1 rounded bg-white/85" />
<div className="rounded bg-white px-2 py-1 font-mono text-sm font-semibold tabular-nums text-neutral-900">
{(cvc || "•••").padEnd(brand === "amex" ? 4 : 3, "•")}
</div>
</div>
<p className="mt-3 text-[9px] uppercase tracking-widest text-white/50">
Security code
</p>
</div>
</div>
</motion.div>
</div>
{/* price lines */}
<div className="mt-auto space-y-2.5 text-sm">
<div className="flex justify-between text-neutral-600 dark:text-neutral-400">
<span>Aurora Pro (annual)</span>
<span className="tabular-nums">$276.00</span>
</div>
<div className="flex justify-between text-emerald-600 dark:text-emerald-400">
<span>Launch discount</span>
<span className="tabular-nums">−$48.00</span>
</div>
<div className="mt-3 flex items-baseline justify-between border-t border-neutral-200 pt-3 dark:border-neutral-800">
<span className="font-semibold">Total due today</span>
<span className="text-xl font-semibold tabular-nums">${AMOUNT}</span>
</div>
</div>
</div>
{/* ---- Right: form / success ---- */}
<div className="relative p-7 sm:p-9">
<AnimatePresence mode="wait" initial={false}>
{status === "success" ? (
<motion.div
key="success"
initial={{ opacity: 0, y: reduce ? 0 : 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={swap}
className="flex min-h-[26rem] flex-col items-center justify-center text-center"
role="status"
aria-live="polite"
>
<div className="fpay-pop flex h-16 w-16 items-center justify-center rounded-full bg-emerald-100 dark:bg-emerald-500/15">
<svg
viewBox="0 0 24 24"
className="h-8 w-8 text-emerald-500 dark:text-emerald-400"
fill="none"
aria-hidden
>
<path
className="fpay-check"
d="M5 12.5l4.2 4.2L19 7"
stroke="currentColor"
strokeWidth="2.4"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
<h2 className="mt-6 text-2xl font-semibold tracking-tight">Payment confirmed</h2>
<p className="mt-2 max-w-xs text-sm text-neutral-500 dark:text-neutral-400">
We charged ${AMOUNT} to your {BRAND_LABEL[brand]} ending in{" "}
<span className="font-mono tabular-nums">{digits.slice(-4) || "0000"}</span>. A
receipt is on its way to your inbox.
</p>
<button
type="button"
onClick={reset}
className="mt-8 rounded-xl border border-neutral-200 bg-white px-5 py-2.5 text-sm font-semibold text-neutral-900 shadow-sm outline-none transition-colors hover:bg-neutral-50 focus-visible:ring-2 focus-visible:ring-indigo-400 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-50 dark:hover:bg-neutral-700"
>
Make another payment
</button>
</motion.div>
) : (
<motion.form
key="form"
noValidate
onSubmit={handleSubmit}
initial={{ opacity: 0, y: reduce ? 0 : 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={swap}
aria-describedby={fid("secnote")}
>
<div className="flex items-center justify-between">
<h1 className="text-lg font-semibold tracking-tight">Payment details</h1>
<div className="flex items-center gap-2 text-neutral-400 dark:text-neutral-500">
<BrandMark brand="visa" className="h-4 text-sm" />
<BrandMark brand="mastercard" className="h-4" />
<BrandMark brand="amex" className="h-4 text-[11px]" />
</div>
</div>
<span className="sr-only" aria-live="polite">
{brand === "unknown" ? "" : `Detected card: ${BRAND_LABEL[brand]}`}
</span>
<div className="mt-6 space-y-4">
{/* card number */}
<div>
<label
htmlFor={fid("number")}
className="mb-1.5 block text-sm font-medium text-neutral-700 dark:text-neutral-300"
>
Card number
</label>
<div className="relative">
<input
id={fid("number")}
ref={numberRef}
type="text"
inputMode="numeric"
autoComplete="cc-number"
placeholder="1234 5678 9012 3456"
value={number}
onChange={onNumber}
onBlur={() => blur("number")}
disabled={status === "processing"}
maxLength={brand === "amex" ? 17 : 19}
aria-invalid={show("number") ? true : undefined}
aria-describedby={show("number") ? fid("number-err") : undefined}
className={`${fieldClass("number")} pr-14 font-mono tracking-wide tabular-nums`}
/>
<span className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-neutral-500 dark:text-neutral-300">
<BrandMark brand={brand} className="h-4 text-sm" />
</span>
</div>
{show("number") ? (
<p
id={fid("number-err")}
role="alert"
className="mt-1.5 text-xs font-medium text-rose-600 dark:text-rose-400"
>
{show("number")}
</p>
) : null}
</div>
{/* name */}
<div>
<label
htmlFor={fid("name")}
className="mb-1.5 block text-sm font-medium text-neutral-700 dark:text-neutral-300"
>
Name on card
</label>
<input
id={fid("name")}
ref={nameRef}
type="text"
autoComplete="cc-name"
placeholder="Jordan A. Rivera"
value={name}
onChange={onName}
onBlur={() => blur("name")}
disabled={status === "processing"}
aria-invalid={show("name") ? true : undefined}
aria-describedby={show("name") ? fid("name-err") : undefined}
className={fieldClass("name")}
/>
{show("name") ? (
<p
id={fid("name-err")}
role="alert"
className="mt-1.5 text-xs font-medium text-rose-600 dark:text-rose-400"
>
{show("name")}
</p>
) : null}
</div>
{/* expiry + cvc + zip */}
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3">
<div>
<label
htmlFor={fid("expiry")}
className="mb-1.5 block text-sm font-medium text-neutral-700 dark:text-neutral-300"
>
Expiry
</label>
<input
id={fid("expiry")}
ref={expiryRef}
type="text"
inputMode="numeric"
autoComplete="cc-exp"
placeholder="MM/YY"
value={expiry}
onChange={onExpiry}
onBlur={() => blur("expiry")}
disabled={status === "processing"}
maxLength={5}
aria-invalid={show("expiry") ? true : undefined}
aria-describedby={show("expiry") ? fid("expiry-err") : undefined}
className={`${fieldClass("expiry")} font-mono tabular-nums`}
/>
{show("expiry") ? (
<p
id={fid("expiry-err")}
role="alert"
className="mt-1.5 text-xs font-medium text-rose-600 dark:text-rose-400"
>
{show("expiry")}
</p>
) : null}
</div>
<div>
<label
htmlFor={fid("cvc")}
className="mb-1.5 block text-sm font-medium text-neutral-700 dark:text-neutral-300"
>
CVC
</label>
<input
id={fid("cvc")}
ref={cvcRef}
type="text"
inputMode="numeric"
autoComplete="cc-csc"
placeholder={brand === "amex" ? "4 digits" : "3 digits"}
value={cvc}
onChange={onCvc}
onFocus={() => setCvcFocused(true)}
onBlur={() => {
setCvcFocused(false);
blur("cvc");
}}
disabled={status === "processing"}
maxLength={brand === "amex" ? 4 : 3}
aria-invalid={show("cvc") ? true : undefined}
aria-describedby={show("cvc") ? fid("cvc-err") : undefined}
className={`${fieldClass("cvc")} font-mono tabular-nums`}
/>
{show("cvc") ? (
<p
id={fid("cvc-err")}
role="alert"
className="mt-1.5 text-xs font-medium text-rose-600 dark:text-rose-400"
>
{show("cvc")}
</p>
) : null}
</div>
<div className="col-span-2 sm:col-span-1">
<label
htmlFor={fid("zip")}
className="mb-1.5 block text-sm font-medium text-neutral-700 dark:text-neutral-300"
>
Billing ZIP
</label>
<input
id={fid("zip")}
ref={zipRef}
type="text"
autoComplete="postal-code"
placeholder="94107"
value={zip}
onChange={onZip}
onBlur={() => blur("zip")}
disabled={status === "processing"}
maxLength={10}
aria-invalid={show("zip") ? true : undefined}
aria-describedby={show("zip") ? fid("zip-err") : undefined}
className={`${fieldClass("zip")} tabular-nums`}
/>
{show("zip") ? (
<p
id={fid("zip-err")}
role="alert"
className="mt-1.5 text-xs font-medium text-rose-600 dark:text-rose-400"
>
{show("zip")}
</p>
) : null}
</div>
</div>
</div>
<button
type="submit"
disabled={status === "processing"}
className="group mt-7 flex w-full items-center justify-center gap-2.5 rounded-xl bg-gradient-to-r from-indigo-600 to-violet-600 px-5 py-3.5 text-sm font-semibold text-white shadow-lg shadow-indigo-600/25 outline-none transition-all duration-150 hover:from-indigo-500 hover:to-violet-500 hover:shadow-indigo-600/40 focus-visible:ring-2 focus-visible:ring-indigo-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-80 dark:focus-visible:ring-offset-neutral-900"
>
{status === "processing" ? (
<>
<span
aria-hidden
className="fpay-spin h-4 w-4 rounded-full border-2 border-white/40 border-t-white"
/>
Processing…
</>
) : (
<>
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" aria-hidden>
<rect
x="5"
y="11"
width="14"
height="9"
rx="2"
stroke="currentColor"
strokeWidth="1.8"
/>
<path
d="M8 11V8a4 4 0 0 1 8 0v3"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
/>
</svg>
Pay ${AMOUNT} securely
</>
)}
</button>
<p
id={fid("secnote")}
className="mt-4 flex items-center justify-center gap-1.5 text-center text-xs text-neutral-500 dark:text-neutral-400"
>
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5 shrink-0" fill="none" aria-hidden>
<rect
x="5"
y="11"
width="14"
height="9"
rx="2"
stroke="currentColor"
strokeWidth="1.8"
/>
<path
d="M8 11V8a4 4 0 0 1 8 0v3"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
/>
</svg>
Secured with 256-bit TLS. Your card is encrypted and never stored on our
servers.
</p>
</motion.form>
)}
</AnimatePresence>
</div>
</div>
</div>
</section>
);
}Dependencies
motion
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Login Form
Originalclean login form with social buttons

Signup Form
Originalsignup form with password strength

Contact Form
Originalcontact form with fields and message

Multistep Form
Originalmulti-step wizard form with progress

Newsletter Form
Originalinline newsletter subscribe form

Search Filters Form
Originalfaceted search filter panel

Profile Form
Originalprofile settings form

OTP Verify Form
OriginalOTP verification form

Upload Dropzone Form
Originaldrag-and-drop file upload with preview

Feedback Form
Originalfeedback form with emoji rating

Survey Form
Originalshort survey with radios and scale

Booking Form
Originalbooking form with date and slots

