Skip to content
oks-ui

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.

checkout.tsx

How it's built

1

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.

2

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.

3

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.

4

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 & paste
checkout.tsx
import { 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&apos;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>
  );
}

More patterns

Commerce
Product gridA responsive book grid with ratings, sale pricing, and a save-for-later action.
Commerce
Invoice detailA full-width invoice builder with a live Preview Drawer for the recipient-facing view.
App screens
Analytics dashboardA full fintech app screen — collapsible sidebar, header search, wallet cards, an earnings chart, savings goals, and a transactions table.