Commerce
Checkout
The checkout screen done as a real two-column flow: a step indicator (Information done, Payment active, Completed upcoming), an order-review Card with per-item thumbnails, a promo code field, quick-apply discount Chips, and a full price breakdown on one side; a live payment-method switch (card vs. virtual account) that swaps the entire form below it on the other.
How it's built
The grid needs its own `@container` on a wrapper, not on the grid element itself — `@3xl:grid-cols-[1fr_1.1fr]` can't be conditioned by the same element that establishes the container (that's circular per the CSS Containment spec), so it would silently never apply.
The payment method toggle is real component state (`method`), not two always-visible sections — picking "Virtual Account" swaps in a completely different form (a bank select) instead of just re-styling a card.
The "Securely save my information" control is the standalone `Checkbox` from `oks-ui/form-field-set` (boolean-shaped, `label`/`description`/`defaultChecked`), not a `FormFieldSet type="checkbox"` — that variant is for a multi-select options array.
The Pay now / Generate virtual account button uses the site's own `bg-ink`/`text-paper` tokens, not a raw oks-ui palette step — those tokens are designed to invert with the theme, so the button stays high-contrast in both light and dark instead of washing out.
Source
One file · copy & pasteimport { useState } from "react";
import { Button } from "oks-ui/button";
import { Card, CardBody, CardHeader } from "oks-ui/card";
import { Checkbox, FormFieldSet } from "oks-ui/form-field-set";
import { Chip } from "oks-ui/chip";
import { Divider } from "oks-ui/divider";
import { BuildingIcon, CreditCardIcon } from "../../showcase/icons";
function ArrowLeftIcon({ size = 16 }: { size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M13 8H3M7 4 3 8l4 4" />
</svg>
);
}
function MapPinIcon({ size = 13 }: { size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M13 6.5c0 3.5-5 8-5 8s-5-4.5-5-8a5 5 0 0 1 10 0Z" />
<circle cx="8" cy="6.5" r="1.5" />
</svg>
);
}
// A checkmark inside a ring — a completed checkout step.
function CheckCircleIcon({ size = 16 }: { size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<circle cx="8" cy="8" r="6.5" />
<path d="M5.3 8.2 7.2 10l3.5-4" />
</svg>
);
}
const items = [
{ id: 1, name: "The Loft Room", location: "2nd Floor, WorkSpace", price: 120, cover: "linear-gradient(160deg, #7dd3b0 0%, #2f6f5a 100%)" },
{ id: 2, name: "Executive Suite", location: "5th Floor, HighTower Plaza", price: 180, cover: "linear-gradient(160deg, #d8b48a 0%, #6b4226 100%)" },
];
const discounts = ["5% Off", "10% Off", "15% Off"];
const STEPS = [
{ key: "info", label: "Information", state: "done" as const },
{ key: "payment", label: "Payment", state: "active" as const },
{ key: "completed", label: "Completed", state: "upcoming" as const },
];
export function Checkout() {
const [method, setMethod] = useState<"card" | "virtual">("card");
return (
// @container has to live on its OWN wrapper, not the grid element
// whose @3xl:grid-cols is conditioned by it — an element can't query
// its own container for its own styling (circular per the CSS
// Containment spec), so that utility would silently be evaluated
// against the wrong (ancestor) container and never apply.
<div className="@container w-full">
<div className="grid gap-8 p-6 @3xl:grid-cols-[1fr_1.1fr] @3xl:p-10">
{/* left — order review */}
<div className="flex flex-col gap-6">
<div>
<p className="text-[1.05rem] font-bold text-ink">Checkout</p>
</div>
<Button variant="link" size="sm" startContent={<ArrowLeftIcon />} className="self-start px-0 text-ink-muted">
Back to cart
</Button>
<Card shadow="sm">
<CardHeader className="flex items-center justify-between">
<span className="font-semibold text-ink">Product Review</span>
<Chip size="sm" variant="soft" color="warning">
Premium
</Chip>
</CardHeader>
<CardBody className="flex flex-col gap-4">
{items.map((item) => (
<div key={item.id} className="flex items-center gap-3">
<div className="h-14 w-14 shrink-0 rounded-[var(--r-md)]" style={{ background: item.cover }} />
<div className="min-w-0 flex-1">
<p className="font-medium text-ink">{item.name}</p>
<p className="flex items-center gap-1 text-[0.8rem] text-ink-muted">
<MapPinIcon />
{item.location}
</p>
</div>
<div className="text-right">
<p className="font-semibold text-ink">${item.price}</p>
<p className="text-[0.75rem] text-ink-faint">/day</p>
</div>
</div>
))}
<Divider />
<div className="flex items-center gap-2">
<FormFieldSet type="text" name="promo" label="Promo code" defaultValue="4ABBS451515" classNames={{ base: "flex-1", label: "sr-only" }} />
<Button variant="bordered" size="sm">
Apply
</Button>
</div>
<div className="flex flex-wrap gap-2">
{discounts.map((d) => (
<Chip key={d} size="sm" variant="soft" color="success">
{d}
</Chip>
))}
</div>
<Divider />
<div className="flex flex-col gap-2 text-[0.9rem]">
<div className="flex justify-between text-ink-soft">
<span>Subtotal</span>
<span>$300.00</span>
</div>
<div className="flex justify-between text-ink-soft">
<span>Discount (50%)</span>
<span>-$60.00</span>
</div>
<div className="flex justify-between text-ink-soft">
<span>Credit (1h)</span>
<span>-$20.00</span>
</div>
<div className="flex items-center justify-between text-ink-soft">
<span className="flex items-center gap-2">
GST (10%)
<Chip size="sm" variant="bordered">
Inclusive
</Chip>
</span>
<span>$22.00</span>
</div>
<Divider />
<div className="flex justify-between text-[1.1rem] font-bold text-ink">
<span>Total</span>
<span>$242.00</span>
</div>
</div>
</CardBody>
</Card>
<p className="text-[0.8rem] text-ink-faint">
<Button variant="link" size="sm" className="px-0 text-ink-faint">
Privacy
</Button>{" "}
|{" "}
<Button variant="link" size="sm" className="px-0 text-ink-faint">
Policy
</Button>
</p>
</div>
{/* right — payment */}
<Card shadow="sm">
<CardBody className="flex flex-col gap-6 p-6 @3xl:p-8">
<div className="flex items-center">
{STEPS.map((step, i) => (
<div key={step.key} className="flex flex-1 items-center last:flex-none">
<div className="flex items-center gap-2">
<span
className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[0.8rem] font-semibold ${
step.state === "done"
? "bg-[color:var(--oks-color-success-100)] text-[color:var(--oks-color-success-700)]"
: step.state === "active"
? "bg-ink text-paper"
: "bg-paper-sunken text-ink-faint"
}`}
>
{step.state === "done" ? <CheckCircleIcon size={16} /> : i + 1}
</span>
<span className={`text-[0.85rem] font-medium ${step.state === "upcoming" ? "text-ink-faint" : "text-ink"}`}>
{step.label}
</span>
</div>
{i < STEPS.length - 1 ? <div className="mx-3 h-px flex-1 bg-line" /> : null}
</div>
))}
</div>
<Divider />
<div>
<h2 className="text-[1.3rem] font-bold text-ink">Enter Payment Details</h2>
</div>
<div>
<p className="mb-2 text-[0.85rem] font-medium text-ink">Select method</p>
<div className="grid grid-cols-2 gap-3">
<Button
variant="bordered"
onClick={() => setMethod("card")}
className={`h-auto flex-col items-start gap-2 p-4 text-left ${method === "card" ? "border-ink" : ""}`}
>
<CreditCardIcon size={18} />
<span className="text-[0.85rem] font-medium text-ink">Debit / Credit Card</span>
</Button>
<Button
variant="bordered"
onClick={() => setMethod("virtual")}
className={`h-auto flex-col items-start gap-2 p-4 text-left ${method === "virtual" ? "border-ink" : ""}`}
>
<BuildingIcon size={18} />
<span className="text-[0.85rem] font-medium text-ink">Virtual Account</span>
</Button>
</div>
</div>
{method === "card" ? (
<div className="flex flex-col gap-4">
<FormFieldSet
type="text"
name="cardNumber"
label="Card information"
placeholder="•••• •••• 1234 1234"
startIcon={<CreditCardIcon size={16} />}
validation={{ rules: { required: true } }}
/>
<div className="grid grid-cols-2 gap-4">
<FormFieldSet type="text" name="expiry" label="Expiry" placeholder="09/28" />
<FormFieldSet type="text" name="cvv" label="CVV" placeholder="123" />
</div>
<FormFieldSet type="text" name="cardholder" label="Cardholder Name" defaultValue="Duran Clayton" validation={{ rules: { required: true } }} />
<FormFieldSet
type="phone"
name="mobile"
label="Mobile"
validation={{ rules: { required: true } }}
/>
<FormFieldSet type="text" name="address" label="Address" placeholder="Enter address manually" />
<Checkbox
defaultChecked
label="Securely save my information for 1-click checkout"
description="Pay faster on every site that accepts this checkout."
/>
<Button color="primary" size="lg" fullWidth className="bg-ink text-paper hover:opacity-90">
Pay now
</Button>
</div>
) : (
<div className="flex flex-col gap-4">
<FormFieldSet
type="select"
name="bank"
label="Bank"
options={[
{ label: "Chase", value: "chase" },
{ label: "Bank of America", value: "boa" },
{ label: "Wells Fargo", value: "wells" },
]}
/>
<p className="text-[0.85rem] text-ink-muted">
We'll generate a virtual account number after you choose your bank — transfer the exact total to complete payment.
</p>
<Button color="primary" size="lg" fullWidth className="bg-ink text-paper hover:opacity-90">
Generate virtual account
</Button>
</div>
)}
</CardBody>
</Card>
</div>
</div>
);
}