Skip to content

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

Built with

How it’s built

  1. 01The 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. 02The 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. 03The "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. 04The 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

Two files · copy & paste

Styling: this source uses Tailwind utility classes and this site’s colour tokens (for example text-ink, bg-paper and border-line). The oks-ui components render without them; swap those classes for your own.

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 "./icons";

const 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>
  );
};

const 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.
const 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 },
];

const 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>
  );
};

export default Checkout;

It imports ./icons, so save this next to it:

icons.tsx
export const BuildingIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="M6 22V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v18" />
      <path d="M2 22h20" />
      <path d="M9 6h1M14 6h1M9 10h1M14 10h1M9 14h1M14 14h1" />
      <path d="M10 22v-4h4v4" />
    </svg>
  );
};

// Icons below this line were added for Nova's rebuild into a fan-community
// events platform -- same "no icon library dependency" convention above.

export const CreditCardIcon = ({ size = 18 }: { size?: number }) => {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <rect x="2" y="5" width="20" height="14" rx="2" />
      <path d="M2 10h20" />
    </svg>
  );
};