Checkout
Card details, billing address and order total.
Checkout
Northwind annual subscription, invoiced to Northwind Ltd.
Payment details
checkout.tsx
"use client";
import { useState } from "react";
import { LuLock } from "react-icons/lu";
import {
Alert,
Button,
CardForm,
Input,
InputPhone,
PaymentCard,
type CardFormValues,
} from "@nexera-ui/react";
const money = new Intl.NumberFormat("en-GB", { style: "currency", currency: "GBP" });
/** A fixed "today", so the expiry check gives the same result on the server and in the browser. */
const TODAY = new Date(2026, 9, 7);
const COUNTRIES = [
{ code: "GB", name: "United Kingdom", dialCode: "+44", flag: "🇬🇧" },
{ code: "IE", name: "Ireland", dialCode: "+353", flag: "🇮🇪" },
{ code: "PT", name: "Portugal", dialCode: "+351", flag: "🇵🇹" },
{ code: "DK", name: "Denmark", dialCode: "+45", flag: "🇩🇰" },
] as const;
const LINES = [
{ label: "Growth plan, 42 employees × £7.20 × 12 months", amount: 3628.8 },
{ label: "Onboarding workshop (one-off)", amount: 450 },
] as const;
const SUBTOTAL = LINES.reduce((sum, line) => sum + line.amount, 0);
const VAT = SUBTOTAL * 0.2;
/** Checkout: card details with a live card preview, billing address and the order total. */
export default function Checkout() {
// Test card only: never put real card numbers in sample code.
const [card, setCard] = useState<CardFormValues>({
number: "4242424242424242",
expiry: "12/29",
cvc: "",
name: "Sara Malik",
});
const [paid, setPaid] = useState<string | null>(null);
return (
<div className="@container bg-page px-4 py-8 @3xl:px-8">
<div className="mx-auto grid w-full max-w-5xl gap-6">
<header className="grid gap-1">
<h2 className="text-heading-h2 text-primary">Checkout</h2>
<p className="text-body-default text-secondary">
Northwind annual subscription, invoiced to Northwind Ltd.
</p>
</header>
{paid ? (
<Alert tone="success" title="Payment confirmed" description={paid} politeness="polite" />
) : null}
<div className="grid gap-6 @5xl:grid-cols-[minmax(0,1fr)_22rem] @5xl:items-start">
<section
aria-labelledby="pay-title"
className="grid gap-5 rounded-xl border border-default bg-surface p-5 shadow-xs @3xl:p-6"
>
<h3 id="pay-title" className="text-heading-h4 text-primary">
Payment details
</h3>
<CardForm
value={card}
onChange={setCard}
today={TODAY}
onSubmit={(values) => {
setPaid(
`${money.format(SUBTOTAL + VAT)} charged to the card ending ${values.number.slice(-4)}. This is a demo, so nothing was sent.`,
);
}}
>
<fieldset className="grid gap-4 border-0 p-0">
<legend className="mb-3 text-heading-h4 text-primary">Billing address</legend>
<Input
label="Company"
name="company"
autoComplete="organization"
defaultValue="Northwind Ltd"
/>
<Input
label="Address"
name="address"
autoComplete="street-address"
defaultValue="18 Canal Street"
/>
<div className="grid gap-4 @3xl:grid-cols-2">
<Input
label="City"
name="city"
autoComplete="address-level2"
defaultValue="London"
/>
<Input
label="Postcode"
name="postcode"
autoComplete="postal-code"
defaultValue="N1 9GU"
/>
</div>
<InputPhone
label="Billing phone"
name="phone"
countries={COUNTRIES}
defaultCountry="GB"
defaultValue="2079460321"
description="We only call about failed payments."
/>
</fieldset>
<Button
type="submit"
size="lg"
leadingIcon={<LuLock />}
className="w-full @3xl:w-auto"
>
Pay {money.format(SUBTOTAL + VAT)}
</Button>
</CardForm>
</section>
<aside
aria-labelledby="order-title"
className="grid gap-5 rounded-xl border border-default bg-surface p-5 shadow-xs"
>
<h3 id="order-title" className="text-heading-h4 text-primary">
Order total
</h3>
<PaymentCard
variant="midnight"
cardName="Company card"
holder={card.name.toUpperCase() || "NAME ON CARD"}
number={card.number}
expiry={card.expiry || "MM/YY"}
className="max-w-80"
/>
<dl className="grid gap-3 text-body-small">
{LINES.map((line) => (
<div key={line.label} className="flex justify-between gap-4">
<dt className="text-secondary">{line.label}</dt>
<dd className="shrink-0 text-primary tabular-nums">
{money.format(line.amount)}
</dd>
</div>
))}
<div className="flex justify-between gap-4 border-t border-subtle pt-3">
<dt className="text-secondary">VAT (20%)</dt>
<dd className="text-primary tabular-nums">{money.format(VAT)}</dd>
</div>
<div className="flex justify-between gap-4 text-body-strong">
<dt className="text-primary">Total due today</dt>
<dd className="text-primary tabular-nums">{money.format(SUBTOTAL + VAT)}</dd>
</div>
</dl>
</aside>
</div>
</div>
</div>
);
}
More commerce and payments blocks
- Pricing plansMonthly or yearly plans with a recommended one.
- Saved payment methodsChoose, add or remove a card.
- Order summaryItems, discounts and totals.
- PayslipEarnings and deductions with totals.
- Product galleryPhotos with thumbnails and zoom.
- Reviews and ratingsAverage score, breakdown and written reviews.
- Order trackingWhere an order is, step by step.