Web InnoventixFreeCode

Address Form

Original · free

shipping address form

byWeb InnoventixReact + Tailwind
formaddressforms
Open

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.json
form-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 &amp; 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 quote

Similar components

Browse all →