Address Form
Original · freeshipping address form
byWeb InnoventixReact + Tailwind
formaddressforms
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-address.jsonform-address.tsx
"use client";
import {
useId,
useState,
type ChangeEvent,
type FocusEvent,
type FormEvent,
type ReactNode,
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
/* ---------------------------------- data ---------------------------------- */
type Region = readonly [code: string, name: string];
type Country = {
readonly code: string;
readonly name: string;
readonly flag: string;
readonly postalLabel: string;
readonly postalExample: string;
readonly postalPattern: RegExp;
readonly postalNumeric: boolean;
readonly regionLabel: string;
readonly regions?: readonly Region[];
};
const US_STATES: readonly Region[] = [
["AL", "Alabama"], ["AK", "Alaska"], ["AZ", "Arizona"], ["AR", "Arkansas"],
["CA", "California"], ["CO", "Colorado"], ["CT", "Connecticut"], ["DE", "Delaware"],
["DC", "District of Columbia"], ["FL", "Florida"], ["GA", "Georgia"], ["HI", "Hawaii"],
["ID", "Idaho"], ["IL", "Illinois"], ["IN", "Indiana"], ["IA", "Iowa"],
["KS", "Kansas"], ["KY", "Kentucky"], ["LA", "Louisiana"], ["ME", "Maine"],
["MD", "Maryland"], ["MA", "Massachusetts"], ["MI", "Michigan"], ["MN", "Minnesota"],
["MS", "Mississippi"], ["MO", "Missouri"], ["MT", "Montana"], ["NE", "Nebraska"],
["NV", "Nevada"], ["NH", "New Hampshire"], ["NJ", "New Jersey"], ["NM", "New Mexico"],
["NY", "New York"], ["NC", "North Carolina"], ["ND", "North Dakota"], ["OH", "Ohio"],
["OK", "Oklahoma"], ["OR", "Oregon"], ["PA", "Pennsylvania"], ["RI", "Rhode Island"],
["SC", "South Carolina"], ["SD", "South Dakota"], ["TN", "Tennessee"], ["TX", "Texas"],
["UT", "Utah"], ["VT", "Vermont"], ["VA", "Virginia"], ["WA", "Washington"],
["WV", "West Virginia"], ["WI", "Wisconsin"], ["WY", "Wyoming"],
];
const CA_PROVINCES: readonly Region[] = [
["AB", "Alberta"], ["BC", "British Columbia"], ["MB", "Manitoba"],
["NB", "New Brunswick"], ["NL", "Newfoundland and Labrador"], ["NS", "Nova Scotia"],
["NT", "Northwest Territories"], ["NU", "Nunavut"], ["ON", "Ontario"],
["PE", "Prince Edward Island"], ["QC", "Quebec"], ["SK", "Saskatchewan"],
["YT", "Yukon"],
];
const COUNTRIES: readonly Country[] = [
{
code: "US",
name: "United States",
flag: "🇺🇸",
postalLabel: "ZIP code",
postalExample: "94103",
postalPattern: /^\d{5}(-\d{4})?$/,
postalNumeric: true,
regionLabel: "State",
regions: US_STATES,
},
{
code: "CA",
name: "Canada",
flag: "🇨🇦",
postalLabel: "Postal code",
postalExample: "M5V 2T6",
postalPattern: /^[A-Za-z]\d[A-Za-z][ -]?\d[A-Za-z]\d$/,
postalNumeric: false,
regionLabel: "Province",
regions: CA_PROVINCES,
},
{
code: "GB",
name: "United Kingdom",
flag: "🇬🇧",
postalLabel: "Postcode",
postalExample: "SW1A 1AA",
postalPattern: /^[A-Za-z]{1,2}\d[A-Za-z\d]?\s?\d[A-Za-z]{2}$/,
postalNumeric: false,
regionLabel: "County",
},
{
code: "AU",
name: "Australia",
flag: "🇦🇺",
postalLabel: "Postcode",
postalExample: "2000",
postalPattern: /^\d{4}$/,
postalNumeric: true,
regionLabel: "State / Territory",
},
{
code: "DE",
name: "Germany",
flag: "🇩🇪",
postalLabel: "Postleitzahl",
postalExample: "10115",
postalPattern: /^\d{5}$/,
postalNumeric: true,
regionLabel: "State",
},
];
type ShipMethod = {
readonly id: string;
readonly name: string;
readonly eta: string;
readonly price: string;
};
const SHIP_METHODS: readonly ShipMethod[] = [
{ id: "standard", name: "Standard", eta: "4–6 business days", price: "Free" },
{ id: "express", name: "Express", eta: "2–3 business days", price: "$12.00" },
{ id: "overnight", name: "Overnight", eta: "Next business day by 12 PM", price: "$28.00" },
];
/* --------------------------------- state ---------------------------------- */
type FieldName =
| "fullName"
| "country"
| "address1"
| "address2"
| "city"
| "region"
| "postal"
| "phone";
type FormValues = Record<FieldName, string>;
type Touched = Partial<Record<FieldName, boolean>>;
type Errors = Partial<Record<FieldName, string>>;
const INITIAL: FormValues = {
fullName: "",
country: "US",
address1: "",
address2: "",
city: "",
region: "",
postal: "",
phone: "",
};
function findCountry(code: string): Country {
return COUNTRIES.find((c) => c.code === code) ?? COUNTRIES[0];
}
function regionName(country: Country, code: string): string {
if (!country.regions) return code;
return country.regions.find((r) => r[0] === code)?.[1] ?? code;
}
function validate(name: FieldName, raw: string, country: Country): string | undefined {
const v = raw.trim();
switch (name) {
case "fullName":
if (!v) return "Enter the recipient's full name.";
if (v.length < 2) return "That name looks too short.";
return;
case "country":
if (!v) return "Choose a country or region.";
return;
case "address1":
if (!v) return "Enter your street address.";
if (v.length < 4) return "Add a house number and street.";
return;
case "city":
if (!v) return "Enter your city or town.";
return;
case "region":
if (country.regions && !v) return `Select your ${country.regionLabel.toLowerCase()}.`;
return;
case "postal":
if (!v) return `Enter your ${country.postalLabel.toLowerCase()}.`;
if (!country.postalPattern.test(v))
return `Use the ${country.name} format, e.g. ${country.postalExample}.`;
return;
case "phone": {
if (!v) return "Enter a phone number for delivery updates.";
const digits = v.replace(/\D/g, "");
if (digits.length < 7) return "That number looks incomplete.";
return;
}
default:
return;
}
}
/* ----------------------------- shared classes ----------------------------- */
const LABEL =
"block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5";
const CONTROL_BASE =
"w-full rounded-xl border bg-white px-3.5 py-2.5 text-[0.95rem] text-slate-900 " +
"placeholder:text-slate-400 shadow-sm outline-none transition-colors " +
"focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-indigo-500 " +
"focus-visible:ring-offset-white dark:bg-slate-950 dark:text-slate-100 " +
"dark:placeholder:text-slate-500 dark:focus-visible:ring-offset-slate-900";
function controlClass(invalid: boolean): string {
return (
CONTROL_BASE +
" " +
(invalid
? "border-rose-400 focus-visible:ring-rose-500 dark:border-rose-500/70"
: "border-slate-300 hover:border-slate-400 dark:border-slate-700 dark:hover:border-slate-600")
);
}
/* ------------------------------ field pieces ------------------------------ */
type BaseFieldProps = {
id: string;
name: FieldName;
label: string;
value: string;
error?: string;
touched?: boolean;
required?: boolean;
hint?: string;
className?: string;
autoComplete?: string;
onBlur: (e: FocusEvent<HTMLInputElement | HTMLSelectElement>) => void;
};
function ErrorLine({ id, message }: { id: string; message: string }) {
const reduce = useReducedMotion();
return (
<AnimatePresence initial={false}>
<motion.p
key="err"
id={id}
role="alert"
initial={reduce ? false : { opacity: 0, height: 0, y: -2 }}
animate={{ opacity: 1, height: "auto", y: 0 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, height: 0, y: -2 }}
transition={{ duration: reduce ? 0 : 0.2, ease: [0.22, 1, 0.36, 1] }}
className="mt-1.5 flex items-start gap-1.5 text-[0.8rem] font-medium text-rose-600 dark:text-rose-400"
>
<svg viewBox="0 0 20 20" className="mt-0.5 h-3.5 w-3.5 flex-none" aria-hidden="true">
<path
fill="currentColor"
d="M10 2a8 8 0 100 16 8 8 0 000-16zm0 4a1 1 0 011 1v4a1 1 0 11-2 0V7a1 1 0 011-1zm0 8.5a1.1 1.1 0 110-2.2 1.1 1.1 0 010 2.2z"
/>
</svg>
<span>{message}</span>
</motion.p>
</AnimatePresence>
);
}
type TextFieldProps = BaseFieldProps & {
type?: "text" | "tel";
inputMode?: "text" | "numeric" | "tel";
placeholder?: string;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
};
function TextField(props: TextFieldProps) {
const {
id, name, label, value, error, touched, required, hint, className,
autoComplete, type = "text", inputMode = "text", placeholder, onChange, onBlur,
} = props;
const invalid = Boolean(touched && error);
const errId = `${id}-err`;
const hintId = `${id}-hint`;
const describedBy = invalid ? errId : hint ? hintId : undefined;
return (
<div className={className}>
<label htmlFor={id} className={LABEL}>
{label}
{required && <span className="ml-0.5 text-indigo-500" aria-hidden="true">*</span>}
</label>
<input
id={id}
name={name}
type={type}
inputMode={inputMode}
value={value}
placeholder={placeholder}
autoComplete={autoComplete}
required={required}
aria-required={required || undefined}
aria-invalid={invalid || undefined}
aria-describedby={describedBy}
onChange={onChange}
onBlur={onBlur}
className={controlClass(invalid)}
/>
{hint && !invalid && (
<p id={hintId} className="mt-1.5 text-[0.8rem] text-slate-500 dark:text-slate-400">
{hint}
</p>
)}
{invalid && error && <ErrorLine id={errId} message={error} />}
</div>
);
}
type SelectFieldProps = BaseFieldProps & {
children: ReactNode;
onChange: (e: ChangeEvent<HTMLSelectElement>) => void;
};
function SelectField(props: SelectFieldProps) {
const {
id, name, label, value, error, touched, required, className,
autoComplete, children, onChange, onBlur,
} = props;
const invalid = Boolean(touched && error);
const errId = `${id}-err`;
return (
<div className={className}>
<label htmlFor={id} className={LABEL}>
{label}
{required && <span className="ml-0.5 text-indigo-500" aria-hidden="true">*</span>}
</label>
<div className="relative">
<select
id={id}
name={name}
value={value}
required={required}
aria-required={required || undefined}
aria-invalid={invalid || undefined}
aria-describedby={invalid ? errId : undefined}
autoComplete={autoComplete}
onChange={onChange}
onBlur={onBlur}
className={controlClass(invalid) + " appearance-none pr-10"}
>
{children}
</select>
<svg
viewBox="0 0 20 20"
aria-hidden="true"
className="pointer-events-none absolute right-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400 dark:text-slate-500"
>
<path fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" d="M6 8l4 4 4-4" />
</svg>
</div>
{invalid && error && <ErrorLine id={errId} message={error} />}
</div>
);
}
/* ------------------------------- component -------------------------------- */
export default function FormAddress() {
const uid = useId();
const [values, setValues] = useState<FormValues>(INITIAL);
const [touched, setTouched] = useState<Touched>({});
const [errors, setErrors] = useState<Errors>({});
const [method, setMethod] = useState<string>("standard");
const [saveDefault, setSaveDefault] = useState<boolean>(true);
const [submitting, setSubmitting] = useState<boolean>(false);
const [done, setDone] = useState<boolean>(false);
const country = findCountry(values.country);
const fid = (n: string) => `${uid}-${n}`;
const setField = (name: FieldName, raw: string) => {
setValues((prev) => {
const next: FormValues = { ...prev, [name]: raw };
if (name === "country") next.region = "";
return next;
});
if (touched[name]) {
const c = name === "country" ? findCountry(raw) : country;
setErrors((prev) => ({ ...prev, [name]: validate(name, raw, c) }));
}
};
const onBlurField = (name: FieldName) => {
setTouched((prev) => ({ ...prev, [name]: true }));
setErrors((prev) => ({ ...prev, [name]: validate(name, values[name], country) }));
};
const runAll = (): boolean => {
const names: FieldName[] = [
"fullName", "country", "address1", "city", "region", "postal", "phone",
];
const nextErrors: Errors = {};
const nextTouched: Touched = {};
for (const n of names) {
nextTouched[n] = true;
const msg = validate(n, values[n], country);
if (msg) nextErrors[n] = msg;
}
setTouched((prev) => ({ ...prev, ...nextTouched }));
setErrors(nextErrors);
return Object.keys(nextErrors).length === 0;
};
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!runAll()) {
const first = document.getElementById(
fid(
(["fullName", "country", "address1", "city", "region", "postal", "phone"] as FieldName[]).find(
(n) => errors[n] ?? validate(n, values[n], country),
) ?? "fullName",
),
);
first?.focus();
return;
}
setSubmitting(true);
window.setTimeout(() => {
setSubmitting(false);
setDone(true);
}, 850);
};
const resetToEdit = () => setDone(false);
const chosen = SHIP_METHODS.find((m) => m.id === method) ?? SHIP_METHODS[0];
return (
<section className="relative w-full overflow-hidden bg-slate-50 px-4 py-16 sm:px-6 sm:py-20 dark:bg-slate-950">
<style>{`
@keyframes fadr-rise {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadr-pop {
0% { transform: scale(.85); opacity: 0; }
60% { transform: scale(1.05); }
100% { transform: scale(1); opacity: 1; }
}
@keyframes fadr-draw {
from { stroke-dashoffset: 26; }
to { stroke-dashoffset: 0; }
}
@keyframes fadr-sheen {
from { background-position: -140% 0; }
to { background-position: 240% 0; }
}
.fadr-rise { animation: fadr-rise .55s cubic-bezier(.22,1,.36,1) both; }
.fadr-pop { animation: fadr-pop .5s cubic-bezier(.22,1,.36,1) both; }
.fadr-draw {
stroke-dasharray: 26; stroke-dashoffset: 26;
animation: fadr-draw .5s .18s cubic-bezier(.65,0,.35,1) forwards;
}
.fadr-sheen {
background-image: linear-gradient(110deg, transparent 30%, rgba(255,255,255,.35) 50%, transparent 70%);
background-size: 200% 100%;
animation: fadr-sheen 1.1s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.fadr-rise, .fadr-pop, .fadr-draw, .fadr-sheen { animation: none !important; }
.fadr-draw { stroke-dashoffset: 0; }
}
`}</style>
{/* atmospheric backdrop */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 -z-10 opacity-70"
style={{
backgroundImage:
"radial-gradient(60rem 40rem at 15% -10%, rgba(99,102,241,0.10), transparent 60%), radial-gradient(50rem 40rem at 100% 10%, rgba(139,92,246,0.10), transparent 55%)",
}}
/>
<div className="mx-auto max-w-2xl">
{/* stepper */}
<nav aria-label="Checkout progress" className="mb-8">
<ol className="flex items-center gap-2 text-xs font-medium">
{[
{ n: "1", label: "Cart", state: "done" as const },
{ n: "2", label: "Shipping", state: "current" as const },
{ n: "3", label: "Payment", state: "todo" as const },
].map((step, i) => (
<li key={step.label} className="flex items-center gap-2">
<span
aria-current={step.state === "current" ? "step" : undefined}
className={
"inline-flex items-center gap-1.5 rounded-full px-3 py-1 " +
(step.state === "current"
? "bg-indigo-600 text-white"
: step.state === "done"
? "bg-indigo-100 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300"
: "bg-slate-100 text-slate-500 dark:bg-slate-800/70 dark:text-slate-400")
}
>
{step.state === "done" ? (
<svg viewBox="0 0 16 16" className="h-3.5 w-3.5" aria-hidden="true">
<path fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" d="M3.5 8.5l3 3 6-7" />
</svg>
) : (
<span aria-hidden="true">{step.n}</span>
)}
{step.label}
</span>
{i < 2 && <span aria-hidden="true" className="h-px w-5 bg-slate-300 dark:bg-slate-700" />}
</li>
))}
</ol>
</nav>
<div className="fadr-rise overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-xl shadow-slate-900/5 dark:border-slate-800 dark:bg-slate-900 dark:shadow-black/30">
{/* header */}
<div className="border-b border-slate-200 px-6 py-5 sm:px-8 dark:border-slate-800">
<div className="flex items-center gap-3">
<span className="inline-flex h-10 w-10 flex-none items-center justify-center rounded-xl bg-indigo-600 text-white shadow-sm shadow-indigo-600/30">
<svg viewBox="0 0 24 24" className="h-5 w-5" aria-hidden="true">
<path fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" d="M3 9.5V17a1 1 0 001 1h1.2M3 9.5l2.2-4.4A1 1 0 016.1 4.5H14v13M3 9.5h11M14 8h4l3 3v6a1 1 0 01-1 1h-1M9 18h6M7 20a2 2 0 100-4 2 2 0 000 4zm11 0a2 2 0 100-4 2 2 0 000 4z" />
</svg>
</span>
<div>
<h2 className="text-lg font-semibold tracking-tight text-slate-900 dark:text-white">
Shipping address
</h2>
<p className="text-sm text-slate-500 dark:text-slate-400">
Where should we send your order?
</p>
</div>
</div>
</div>
{done ? (
/* -------------------------- confirmation -------------------------- */
<div className="px-6 py-10 sm:px-8">
<div className="flex flex-col items-center text-center">
<span className="fadr-pop inline-flex h-16 w-16 items-center justify-center rounded-full bg-emerald-100 dark:bg-emerald-500/15">
<svg viewBox="0 0 32 32" className="h-8 w-8 text-emerald-600 dark:text-emerald-400" aria-hidden="true">
<path className="fadr-draw" fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" d="M9 16.5l4.5 4.5L23 11" />
</svg>
</span>
<h3 className="mt-5 text-xl font-semibold tracking-tight text-slate-900 dark:text-white">
Address saved
</h3>
<p className="mt-1.5 max-w-sm text-sm text-slate-500 dark:text-slate-400">
We'll ship your order to the address below with {chosen.name.toLowerCase()} delivery
({chosen.eta.toLowerCase()}).
</p>
</div>
<dl className="mt-7 rounded-xl border border-slate-200 bg-slate-50 p-5 text-sm dark:border-slate-800 dark:bg-slate-950/60">
<div className="grid grid-cols-[7rem_1fr] gap-y-2.5">
<dt className="text-slate-500 dark:text-slate-400">Recipient</dt>
<dd className="font-medium text-slate-900 dark:text-slate-100">{values.fullName}</dd>
<dt className="text-slate-500 dark:text-slate-400">Address</dt>
<dd className="text-slate-900 dark:text-slate-100">
{values.address1}
{values.address2 ? <>, {values.address2}</> : null}
<br />
{values.city}, {regionName(country, values.region)} {values.postal}
<br />
{country.flag} {country.name}
</dd>
<dt className="text-slate-500 dark:text-slate-400">Phone</dt>
<dd className="text-slate-900 dark:text-slate-100">{values.phone}</dd>
<dt className="text-slate-500 dark:text-slate-400">Delivery</dt>
<dd className="text-slate-900 dark:text-slate-100">
{chosen.name} — {chosen.price}
</dd>
</div>
</dl>
<div className="mt-6 flex flex-col gap-3 sm:flex-row-reverse">
<button
type="button"
className="inline-flex w-full items-center justify-center gap-2 rounded-xl bg-indigo-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm shadow-indigo-600/25 transition-colors hover:bg-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white sm:w-auto dark:focus-visible:ring-offset-slate-900"
>
Continue to payment
<svg viewBox="0 0 20 20" className="h-4 w-4" aria-hidden="true">
<path fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" d="M4 10h12M11 5l5 5-5 5" />
</svg>
</button>
<button
type="button"
onClick={resetToEdit}
className="inline-flex w-full items-center justify-center rounded-xl border border-slate-300 bg-white px-5 py-2.5 text-sm font-semibold text-slate-700 transition-colors hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white sm:w-auto dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-900"
>
Edit address
</button>
</div>
</div>
) : (
/* ------------------------------ form ------------------------------ */
<form noValidate onSubmit={onSubmit} className="px-6 py-6 sm:px-8 sm:py-7">
<div className="grid grid-cols-1 gap-x-4 gap-y-5 sm:grid-cols-2">
<TextField
className="sm:col-span-2"
id={fid("fullName")}
name="fullName"
label="Full name"
placeholder="Jordan Alvarez"
autoComplete="name"
required
value={values.fullName}
error={errors.fullName}
touched={touched.fullName}
onChange={(e) => setField("fullName", e.target.value)}
onBlur={() => onBlurField("fullName")}
/>
<SelectField
className="sm:col-span-2"
id={fid("country")}
name="country"
label="Country / region"
autoComplete="country-name"
required
value={values.country}
error={errors.country}
touched={touched.country}
onChange={(e) => setField("country", e.target.value)}
onBlur={() => onBlurField("country")}
>
{COUNTRIES.map((c) => (
<option key={c.code} value={c.code}>
{c.flag} {c.name}
</option>
))}
</SelectField>
<TextField
className="sm:col-span-2"
id={fid("address1")}
name="address1"
label="Street address"
placeholder="1600 Market Street"
autoComplete="address-line1"
required
value={values.address1}
error={errors.address1}
touched={touched.address1}
onChange={(e) => setField("address1", e.target.value)}
onBlur={() => onBlurField("address1")}
/>
<TextField
className="sm:col-span-2"
id={fid("address2")}
name="address2"
label="Apartment, suite, etc."
hint="Optional — floor, unit, or delivery note."
placeholder="Suite 400"
autoComplete="address-line2"
value={values.address2}
onChange={(e) => setField("address2", e.target.value)}
onBlur={() => onBlurField("address2")}
/>
<TextField
id={fid("city")}
name="city"
label="City"
placeholder="San Francisco"
autoComplete="address-level2"
required
value={values.city}
error={errors.city}
touched={touched.city}
onChange={(e) => setField("city", e.target.value)}
onBlur={() => onBlurField("city")}
/>
{country.regions ? (
<SelectField
id={fid("region")}
name="region"
label={country.regionLabel}
autoComplete="address-level1"
required
value={values.region}
error={errors.region}
touched={touched.region}
onChange={(e) => setField("region", e.target.value)}
onBlur={() => onBlurField("region")}
>
<option value="" disabled>
Select {country.regionLabel.toLowerCase()}
</option>
{country.regions.map((r) => (
<option key={r[0]} value={r[0]}>
{r[1]}
</option>
))}
</SelectField>
) : (
<TextField
id={fid("region")}
name="region"
label={country.regionLabel}
hint="Optional for this country."
placeholder={country.regionLabel}
autoComplete="address-level1"
value={values.region}
onChange={(e) => setField("region", e.target.value)}
onBlur={() => onBlurField("region")}
/>
)}
<TextField
id={fid("postal")}
name="postal"
label={country.postalLabel}
placeholder={country.postalExample}
inputMode={country.postalNumeric ? "numeric" : "text"}
autoComplete="postal-code"
required
value={values.postal}
error={errors.postal}
touched={touched.postal}
onChange={(e) => setField("postal", e.target.value)}
onBlur={() => onBlurField("postal")}
/>
<TextField
id={fid("phone")}
name="phone"
label="Phone"
type="tel"
inputMode="tel"
placeholder="(415) 555-0132"
autoComplete="tel"
required
hint="Used only for delivery updates."
value={values.phone}
error={errors.phone}
touched={touched.phone}
onChange={(e) => setField("phone", e.target.value)}
onBlur={() => onBlurField("phone")}
/>
</div>
{/* shipping method */}
<fieldset className="mt-7">
<legend className="text-sm font-semibold text-slate-900 dark:text-white">
Delivery speed
</legend>
<div className="mt-3 grid gap-2.5">
{SHIP_METHODS.map((m) => {
const active = method === m.id;
return (
<label
key={m.id}
className={
"group flex cursor-pointer items-center gap-3 rounded-xl border px-4 py-3 transition-colors " +
(active
? "border-indigo-500 bg-indigo-50/70 ring-1 ring-indigo-500 dark:border-indigo-400/70 dark:bg-indigo-500/10 dark:ring-indigo-400/60"
: "border-slate-300 hover:border-slate-400 dark:border-slate-700 dark:hover:border-slate-600")
}
>
<input
type="radio"
name="shipping-method"
value={m.id}
checked={active}
onChange={() => setMethod(m.id)}
className="peer sr-only"
/>
<span
aria-hidden="true"
className={
"grid h-5 w-5 flex-none place-items-center rounded-full border-2 transition-colors " +
(active
? "border-indigo-600 dark:border-indigo-400"
: "border-slate-400 group-hover:border-slate-500 dark:border-slate-600")
}
>
<span
className={
"h-2.5 w-2.5 rounded-full bg-indigo-600 transition-transform dark:bg-indigo-400 " +
(active ? "scale-100" : "scale-0")
}
/>
</span>
<span className="flex-1">
<span className="block text-sm font-medium text-slate-900 dark:text-slate-100">
{m.name}
</span>
<span className="block text-xs text-slate-500 dark:text-slate-400">
{m.eta}
</span>
</span>
<span
className={
"text-sm font-semibold " +
(m.price === "Free"
? "text-emerald-600 dark:text-emerald-400"
: "text-slate-900 dark:text-slate-100")
}
>
{m.price}
</span>
</label>
);
})}
</div>
</fieldset>
{/* save default */}
<label className="mt-5 flex cursor-pointer items-start gap-3">
<span className="relative mt-0.5 inline-flex h-5 w-5 flex-none">
<input
type="checkbox"
checked={saveDefault}
onChange={(e) => setSaveDefault(e.target.checked)}
className="peer h-5 w-5 cursor-pointer appearance-none rounded-md border border-slate-300 bg-white transition-colors checked:border-indigo-600 checked:bg-indigo-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-600 dark:bg-slate-950 dark:checked:border-indigo-500 dark:checked:bg-indigo-500 dark:focus-visible:ring-offset-slate-900"
/>
<svg
viewBox="0 0 16 16"
aria-hidden="true"
className="pointer-events-none absolute inset-0 m-auto h-3 w-3 text-white opacity-0 peer-checked:opacity-100"
>
<path fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" d="M3 8.5l3.2 3.2L13 5" />
</svg>
</span>
<span className="text-sm text-slate-600 dark:text-slate-300">
Set as my default shipping address
</span>
</label>
{/* submit */}
<div className="mt-7 border-t border-slate-200 pt-6 dark:border-slate-800">
<button
type="submit"
disabled={submitting}
className="relative inline-flex w-full items-center justify-center gap-2 overflow-hidden rounded-xl bg-indigo-600 px-5 py-3 text-sm font-semibold text-white shadow-sm shadow-indigo-600/25 transition-colors hover:bg-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-90 dark:focus-visible:ring-offset-slate-900"
>
{submitting && (
<span aria-hidden="true" className="fadr-sheen absolute inset-0" />
)}
{submitting ? (
<>
<svg viewBox="0 0 24 24" className="h-4 w-4 animate-spin" aria-hidden="true">
<circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" strokeWidth="3" opacity="0.25" />
<path fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" d="M21 12a9 9 0 00-9-9" />
</svg>
Saving address…
</>
) : (
<>
Save & continue
<svg viewBox="0 0 20 20" className="h-4 w-4" aria-hidden="true">
<path fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" d="M4 10h12M11 5l5 5-5 5" />
</svg>
</>
)}
</button>
<p className="mt-3 flex items-center justify-center gap-1.5 text-xs text-slate-400 dark:text-slate-500">
<svg viewBox="0 0 20 20" className="h-3.5 w-3.5" aria-hidden="true">
<path fill="currentColor" d="M10 1l7 3v5c0 4.5-3 8.3-7 10-4-1.7-7-5.5-7-10V4l7-3zm0 2.2L5 5.3V9c0 3.4 2.1 6.4 5 7.8 2.9-1.4 5-4.4 5-7.8V5.3l-5-2.1z" />
</svg>
Encrypted and never shared with couriers beyond delivery.
</p>
</div>
</form>
)}
</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

Payment Form
Originalcard payment form with formatting

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

