# Bag Checkout

> A three-step checkout with delivery, payment, and review steps beside an order summary.

A shadcn/ui block from the [@esuiss](https://suiss.app/ui?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/esuiss/bag-checkout).

- Page, with a live preview: https://shadcn.exchange/r/esuiss/bag-checkout
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/esuiss/bag-checkout-light-da0b481219fa4fe6.png
- Registry item: https://suiss.app/ui/r/new-york-v4/bag-checkout.json
- Categories: blocks, bag
- Listed: 2026-10-20, source last changed 2026-10-20

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/esuiss/bag-checkout.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `@esuiss/button`, `@esuiss/label`, `@esuiss/radio-group`

## Files

### `components/blocks/bag-checkout.tsx`

```tsx
"use client"

import * as React from "react"

import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"

const photo = (id: string) =>
  `https://images.unsplash.com/photo-${id}?w=200&q=80&auto=format&fit=crop`

const items = [
  {
    name: "Phone Pro",
    variant: "256 GB · Cosmic Orange",
    price: 1099,
    image: photo("1714972384975-fbd4f19b1bb3"),
  },
  {
    name: "Pods Studio",
    variant: "Midnight",
    price: 549,
    image: photo("1599669454699-248893623440"),
  },
  {
    name: "Phone Pro Clear Case",
    variant: "With MagCharge",
    price: 49,
    image: photo("1703676311066-34caf2cbee33"),
  },
]

const money = (value: number) =>
  value.toLocaleString("en-US", { style: "currency", currency: "USD" })

const choiceClass =
  "flex cursor-pointer items-center gap-3 rounded-[0.875rem] px-4 py-3.5 text-base font-normal shadow-[inset_0_0_0_1px_var(--color-input)] transition-shadow duration-200 has-data-checked:shadow-[inset_0_0_0_2px_var(--color-primary)] has-focus-visible:ring-3 has-focus-visible:ring-ring/50"

function stepState(index: number, current: number) {
  if (current > index) return "done"
  if (current === index) return "open"
  return "idle"
}

function Step({
  index,
  current,
  title,
  summary,
  onEdit,
  children,
}: {
  index: number
  current: number
  title: string
  summary?: string
  onEdit: () => void
  children: React.ReactNode
}) {
  const open = current === index
  const done = current > index

  return (
    <section
      aria-current={open ? "step" : undefined}
      className="border-b border-border py-4.5"
    >
      <div className="flex items-center gap-2.5">
        <span
          data-state={stepState(index, current)}
          className="flex size-6.5 items-center justify-center rounded-full bg-secondary text-sm font-semibold text-muted-foreground data-[state=done]:bg-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_35%)] data-[state=done]:text-white data-[state=open]:bg-foreground data-[state=open]:text-background dark:data-[state=done]:bg-[color-mix(in_oklab,var(--color-green-500),black_35%)]"
        >
          {done ? (
            <IconPlaceholder
              lucide="CheckIcon"
              tabler="IconCheck"
              hugeicons="Tick02Icon"
              phosphor="CheckIcon"
              remixicon="RiCheckLine"
              className="size-3.25"
            />
          ) : (
            index + 1
          )}
        </span>
        <h2
          data-idle={!open && !done ? "" : undefined}
          className="flex-1 text-2xl font-semibold data-idle:text-muted-foreground"
        >
          {title}
        </h2>
        {done && (
          <button
            type="button"
            onClick={onEdit}
            aria-label={`Edit ${title.toLowerCase()}`}
            className="rounded-xs text-sm text-primary outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring/50"
          >
            Edit ›
          </button>
        )}
      </div>
      {done && summary && (
        <p className="ps-9 pt-1.5 text-sm text-muted-foreground">{summary}</p>
      )}
      <div
        data-open={open ? "" : undefined}
        inert={!open}
        className="grid grid-rows-[0fr] transition-[grid-template-rows] duration-450 ease-[cubic-bezier(0.32,0.72,0,1)] data-open:grid-rows-[1fr] motion-reduce:transition-none"
      >
        <div className="min-h-0 overflow-hidden">
          <div className="flex flex-col gap-2.5 ps-9 pt-4 pb-1">{children}</div>
        </div>
      </div>
    </section>
  )
}

export function BagCheckout() {
  const [step, setStep] = React.useState(0)
  const [shipping, setShipping] = React.useState("standard")
  const [payment, setPayment] = React.useState("card")
  const [placed, setPlaced] = React.useState(false)
  const total =
    items.reduce((sum, item) => sum + item.price, 0) +
    (shipping === "express" ? 19 : 0)

  if (placed) {
    return (
      <section className="flex min-h-160 items-center justify-center p-6">
        <div className="flex max-w-105 flex-col items-center gap-3.5 text-center transition-[opacity,translate] duration-1000 ease-[cubic-bezier(0.32,0.72,0,1)] starting:translate-y-4.5 starting:opacity-0 motion-reduce:transition-none">
          <span className="flex size-19 items-center justify-center rounded-full bg-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_25%)] text-white dark:bg-[color-mix(in_oklab,var(--color-green-500),black_35%)]">
            <IconPlaceholder
              lucide="CheckIcon"
              tabler="IconCheck"
              hugeicons="Tick02Icon"
              phosphor="CheckIcon"
              remixicon="RiCheckLine"
              className="size-9.5"
            />
          </span>
          <h1 className="text-4xl font-semibold tracking-tight">
            Thank you. Your order is in.
          </h1>
          <p className="text-lg text-muted-foreground">
            Your order number is W1049. We sent a confirmation email.
          </p>
          <button
            type="button"
            onClick={() => {
              setPlaced(false)
              setStep(0)
            }}
            className="rounded-xs text-lg text-primary outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring/50"
          >
            Start over ›
          </button>
        </div>
      </section>
    )
  }

  return (
    <section className="grid min-h-160 bg-background md:grid-cols-[minmax(0,1.4fr)_minmax(16.25rem,1fr)]">
      <div className="px-5 py-6 md:px-9">
        <h1 className="mb-2 text-3xl font-semibold tracking-tight">Checkout</h1>
        <Step
          index={0}
          current={step}
          title="Delivery"
          summary={
            shipping === "express"
              ? "Express · Tomorrow"
              : "Standard · 3–5 business days"
          }
          onEdit={() => setStep(0)}
        >
          <RadioGroup
            value={shipping}
            onValueChange={(value) => setShipping(value as string)}
            aria-label="Delivery"
            className="gap-2.5"
          >
            <Label className={choiceClass}>
              <RadioGroupItem
                value="standard"
                className="size-5 focus-visible:ring-0"
              />
              <span className="flex-1">Standard · 3–5 business days</span>
              <span>Free</span>
            </Label>
            <Label className={choiceClass}>
              <RadioGroupItem
                value="express"
                className="size-5 focus-visible:ring-0"
              />
              <span className="flex-1">Express · Tomorrow</span>
              <span>$19</span>
            </Label>
          </RadioGroup>
          <Button size="sm" className="self-start" onClick={() => setStep(1)}>
            Continue to Payment
          </Button>
        </Step>
        <Step
          index={1}
          current={step}
          title="Payment"
          summary={payment === "card" ? "Card •••• 4242" : "suiss Pay"}
          onEdit={() => setStep(1)}
        >
          <RadioGroup
            value={payment}
            onValueChange={(value) => setPayment(value as string)}
            aria-label="Payment"
            className="gap-2.5"
          >
            <Label className={choiceClass}>
              <RadioGroupItem
                value="card"
                className="size-5 focus-visible:ring-0"
              />
              <IconPlaceholder
                lucide="CreditCardIcon"
                tabler="IconCreditCard"
                hugeicons="CreditCardIcon"
                phosphor="CreditCardIcon"
                remixicon="RiBankCardLine"
                className="size-4.5"
              />
              <span className="flex-1">Credit card · •••• 4242</span>
            </Label>
            <Label className={choiceClass}>
              <RadioGroupItem
                value="pay"
                className="size-5 focus-visible:ring-0"
              />
              <IconPlaceholder
                lucide="WalletIcon"
                tabler="IconWallet"
                hugeicons="Wallet01Icon"
                phosphor="WalletIcon"
                remixicon="RiWalletLine"
                className="size-4.5"
              />
              <span className="flex-1">suiss Pay</span>
            </Label>
          </RadioGroup>
          <Button size="sm" className="self-start" onClick={() => setStep(2)}>
            Continue to Review
          </Button>
        </Step>
        <Step index={2} current={step} title="Review" onEdit={() => setStep(2)}>
          <p className="text-sm leading-normal text-muted-foreground">
            By placing your order, you agree to the Sales Policy.
          </p>
          <Button className="self-start" onClick={() => setPlaced(true)}>
            Place Order
          </Button>
        </Step>
      </div>
      <aside
        aria-label="Order summary"
        className="flex flex-col gap-3.5 bg-muted px-6.5 py-7"
      >
        <h2 className="text-lg font-semibold">Order summary</h2>
        <ul className="flex flex-col gap-3.5">
          {items.map((item) => (
            <li key={item.name} className="flex items-center gap-2.5">
              <img
                src={item.image}
                alt=""
                className="size-12 shrink-0 rounded-[0.625rem] bg-secondary object-cover"
              />
              <span className="flex min-w-0 flex-1 flex-col">
                <span className="text-sm font-semibold">{item.name}</span>
                <span className="text-xs text-muted-foreground">
                  {item.variant}
                </span>
              </span>
              <span className="text-sm tabular-nums">{money(item.price)}</span>
            </li>
          ))}
        </ul>
        <div className="flex items-center border-t border-border pt-3">
          <span className="flex-1 text-base font-semibold">Total</span>
          <span
            className="text-lg font-semibold tabular-nums"
            aria-live="polite"
          >
            {money(total)}
          </span>
        </div>
      </aside>
    </section>
  )
}
```
