Theme

Checkout

  1. Cart Review
  2. Shipping
  3. Payment
  4. Confirmation

Your Cart

Product Qty Unit Price Line Total
Wireless Noise-Cancelling Headphones
$149.99 $149.99
Mechanical Keyboard (TKL)
$89.95 $89.95
USB-C Docking Station
$64.50 $129.00
Ergonomic Mouse Pad (XL)
$24.99 $24.99

Order Summary

Subtotal $393.93
Shipping Calculated next step
Tax (8.5%) $33.48
Estimated Total $433.40

Source

This page is rendered from the real demo file shown below.

svelte
<script lang="ts">
  import {
    Alert,
    Badge,
    Button,
    Card,
    Checkbox,
    Field,
    Flex,
    Grid,
    Input,
    Label,
    NumberInput,
    RadioGroup,
    Select,
    Separator,
    Stack,
    Stepper,
    Table,
  } from '@dryui/ui';
  import styles from '$lib/components/docs-demo.module.css';

  // ─── Step state ───────────────────────────────────────────────────────────
  let currentStep = $state(0);
  const STEPS = ['Cart Review', 'Shipping', 'Payment', 'Confirmation'];

  // ─── Cart ─────────────────────────────────────────────────────────────────
  type CartItem = { id: number; name: string; price: number; qty: number };

  let cartItems = $state<CartItem[]>([
    { id: 1, name: 'Wireless Noise-Cancelling Headphones', price: 149.99, qty: 1 },
    { id: 2, name: 'Mechanical Keyboard (TKL)',            price: 89.95,  qty: 1 },
    { id: 3, name: 'USB-C Docking Station',                price: 64.50,  qty: 2 },
    { id: 4, name: 'Ergonomic Mouse Pad (XL)',             price: 24.99,  qty: 1 },
  ]);

  // ─── Shipping ─────────────────────────────────────────────────────────────
  // Declare shippingMethod before derived values that reference it
  let shippingMethod = $state('standard');

  let subtotal  = $derived(cartItems.reduce((s, i) => s + i.price * i.qty, 0));
  let shippingCost = $derived(
    shippingMethod === 'express'   ? 12.99 :
    shippingMethod === 'overnight' ? 24.99 : 5.99
  );
  let tax   = $derived(subtotal * 0.085);
  let total = $derived(subtotal + shippingCost + tax);

  function fmt(n: number) {
    return n.toLocaleString('en-US', { style: 'currency', currency: 'USD' });
  }

  let firstName    = $state('');
  let lastName     = $state('');
  let email        = $state('');
  let phone        = $state('');
  let address      = $state('');
  let city         = $state('');
  let stateRegion  = $state('');
  let zip          = $state('');
  let country      = $state('US');

  const shippingOptions = [
    { value: 'standard',  label: 'Standard Shipping',  eta: '5–7 business days', price: '$5.99'  },
    { value: 'express',   label: 'Express Shipping',   eta: '2–3 business days', price: '$12.99' },
    { value: 'overnight', label: 'Overnight Delivery', eta: 'Next business day', price: '$24.99' },
  ];

  // ─── Payment ──────────────────────────────────────────────────────────────
  let paymentMethod      = $state('card');
  let cardNumber         = $state('');
  let cardExpiry         = $state('');
  let cardCvv            = $state('');
  let savePayment        = $state(false);
  let billingSameAsShip  = $state(true);

  // ─── Navigation ───────────────────────────────────────────────────────────
  function goBack()     { if (currentStep > 0) currentStep--; }
  function goContinue() { if (currentStep < STEPS.length - 1) currentStep++; }

  // ─── Order number ─────────────────────────────────────────────────────────
  const orderNumber = `ORD-${Math.floor(100000 + Math.random() * 900000)}`;
</script>

<Stack gap="lg">
  <h2>Checkout</h2>

  <!-- ── Stepper ─────────────────────────────────────────────────────────── -->
  <Card.Root>
    <Card.Content>
      <Stepper.Root bind:activeStep={currentStep}>
        <Stepper.List>
          {#each STEPS as label, i}
            <Stepper.Step step={i}>
              {label}
            </Stepper.Step>
            {#if i < STEPS.length - 1}
              <Stepper.Separator step={i} />
            {/if}
          {/each}
        </Stepper.List>
      </Stepper.Root>
    </Card.Content>
  </Card.Root>

  <!-- ── Step 0: Cart Review ─────────────────────────────────────────────── -->
  {#if currentStep === 0}
    <Flex gap="lg" align="start">
      <!-- Cart table takes remaining space -->
      <div class="cart-main">
        <Card.Root>
          <Card.Header>
            <h3>Your Cart</h3>
          </Card.Header>
          <Card.Content>
            <Table.Root>
              <Table.Header>
                <Table.Row>
                  <Table.Head>Product</Table.Head>
                  <Table.Head>Qty</Table.Head>
                  <Table.Head>Unit Price</Table.Head>
                  <Table.Head>Line Total</Table.Head>
                </Table.Row>
              </Table.Header>
              <Table.Body>
                {#each cartItems as item (item.id)}
                  <Table.Row>
                    <Table.Cell>
                      <span class="product-name">{item.name}</span>
                    </Table.Cell>
                    <Table.Cell>
                      <NumberInput
                        bind:value={item.qty}
                        min={1}
                        max={99}
                        step={1}
                        size="sm"
                      />
                    </Table.Cell>
                    <Table.Cell>{fmt(item.price)}</Table.Cell>
                    <Table.Cell>
                      <strong>{fmt(item.price * item.qty)}</strong>
                    </Table.Cell>
                  </Table.Row>
                {/each}
              </Table.Body>
            </Table.Root>
          </Card.Content>
        </Card.Root>
      </div>

      <!-- Order summary -->
      <div class="summary-sidebar">
      <Card.Root>
        <Card.Header>
          <h3>Order Summary</h3>
        </Card.Header>
        <Card.Content>
          <Stack gap="sm">
            <Flex justify="between">
              <span class="summary-label">Subtotal</span>
              <span>{fmt(subtotal)}</span>
            </Flex>
            <Flex justify="between">
              <span class="summary-label">Shipping</span>
              <span class="muted">Calculated next step</span>
            </Flex>
            <Flex justify="between">
              <span class="summary-label">Tax (8.5%)</span>
              <span>{fmt(tax)}</span>
            </Flex>
            <Separator />
            <Flex justify="between">
              <strong>Estimated Total</strong>
              <strong>{fmt(subtotal + 5.99 + tax)}</strong>
            </Flex>
          </Stack>
        </Card.Content>
        <Card.Footer>
          <Badge variant="soft" color="blue">
            {cartItems.reduce((n, i) => n + i.qty, 0)} items
          </Badge>
        </Card.Footer>
      </Card.Root>
      </div>
    </Flex>
  {/if}

  <!-- ── Step 1: Shipping ────────────────────────────────────────────────── -->
  {#if currentStep === 1}
    <Grid columns={2} gap="lg">
      <Card.Root>
        <Card.Header><h3>Delivery Address</h3></Card.Header>
        <Card.Content>
          <Stack gap="md">
            <Grid columns={2} gap="md">
              <Field.Root>
                <Label>First Name</Label>
                <Input bind:value={firstName} placeholder="Jane" />
              </Field.Root>
              <Field.Root>
                <Label>Last Name</Label>
                <Input bind:value={lastName} placeholder="Smith" />
              </Field.Root>
            </Grid>

            <Field.Root>
              <Label>Email Address</Label>
              <Input type="email" bind:value={email} placeholder="jane@example.com" />
            </Field.Root>

            <Field.Root>
              <Label>Phone Number</Label>
              <Input type="tel" bind:value={phone} placeholder="+1 (555) 000-0000" />
            </Field.Root>

            <Field.Root>
              <Label>Street Address</Label>
              <Input bind:value={address} placeholder="123 Main St, Apt 4B" />
            </Field.Root>

            <Grid columns={2} gap="md">
              <Field.Root>
                <Label>City</Label>
                <Input bind:value={city} placeholder="New York" />
              </Field.Root>
              <Field.Root>
                <Label>ZIP / Postal Code</Label>
                <Input bind:value={zip} placeholder="10001" />
              </Field.Root>
            </Grid>

            <Grid columns={2} gap="md">
              <Field.Root>
                <Label>State / Region</Label>
                <Input bind:value={stateRegion} placeholder="NY" />
              </Field.Root>
              <Field.Root>
                <Label>Country</Label>
                <Select.Root bind:value={country}>
                  <Select.Trigger>
                    <Button variant="outline" class={styles.fillWidthControl}>
                      <Select.Value placeholder="Select country" />
                    </Button>
                  </Select.Trigger>
                  <Select.Content>
                    <Select.Item value="US">United States</Select.Item>
                    <Select.Item value="CA">Canada</Select.Item>
                    <Select.Item value="GB">United Kingdom</Select.Item>
                    <Select.Item value="AU">Australia</Select.Item>
                    <Select.Item value="DE">Germany</Select.Item>
                    <Select.Item value="FR">France</Select.Item>
                    <Select.Item value="JP">Japan</Select.Item>
                  </Select.Content>
                </Select.Root>
              </Field.Root>
            </Grid>
          </Stack>
        </Card.Content>
      </Card.Root>

      <Card.Root>
        <Card.Header><h3>Shipping Method</h3></Card.Header>
        <Card.Content>
          <RadioGroup.Root bind:value={shippingMethod}>
            <Stack gap="sm">
              {#each shippingOptions as opt}
                <label class="shipping-option" class:selected={shippingMethod === opt.value}>
                  <RadioGroup.Item value={opt.value} />
                  <Stack gap="sm" class={styles.flexGrow}>
                    <Flex justify="between" align="center">
                      <strong>{opt.label}</strong>
                      <Badge variant="outline">{opt.price}</Badge>
                    </Flex>
                    <span class="muted">{opt.eta}</span>
                  </Stack>
                </label>
              {/each}
            </Stack>
          </RadioGroup.Root>
        </Card.Content>

        <Separator />

        <Card.Content>
          <Stack gap="sm">
            <Flex justify="between">
              <span class="summary-label">Subtotal</span>
              <span>{fmt(subtotal)}</span>
            </Flex>
            <Flex justify="between">
              <span class="summary-label">Shipping</span>
              <span>{fmt(shippingCost)}</span>
            </Flex>
            <Flex justify="between">
              <span class="summary-label">Tax</span>
              <span>{fmt(tax)}</span>
            </Flex>
            <Separator />
            <Flex justify="between">
              <strong>Total</strong>
              <strong>{fmt(total)}</strong>
            </Flex>
          </Stack>
        </Card.Content>
      </Card.Root>
    </Grid>
  {/if}

  <!-- ── Step 2: Payment ─────────────────────────────────────────────────── -->
  {#if currentStep === 2}
    <Grid columns={2} gap="lg">
      <Card.Root>
        <Card.Header><h3>Payment Method</h3></Card.Header>
        <Card.Content>
          <Stack gap="lg">
            <RadioGroup.Root bind:value={paymentMethod}>
              <Stack gap="sm">
                <label class="payment-option" class:selected={paymentMethod === 'card'}>
                  <RadioGroup.Item value="card" />
                  <Stack gap="sm">
                    <strong>Credit / Debit Card</strong>
                    <span class="muted">Visa, Mastercard, Amex, Discover</span>
                  </Stack>
                </label>

                <label class="payment-option" class:selected={paymentMethod === 'paypal'}>
                  <RadioGroup.Item value="paypal" />
                  <Stack gap="sm">
                    <strong>PayPal</strong>
                    <span class="muted">Pay via your PayPal balance or linked bank account</span>
                  </Stack>
                </label>

                <label class="payment-option" class:selected={paymentMethod === 'bank'}>
                  <RadioGroup.Item value="bank" />
                  <Stack gap="sm">
                    <strong>Bank Transfer</strong>
                    <span class="muted">ACH direct debit — 12 business days</span>
                  </Stack>
                </label>
              </Stack>
            </RadioGroup.Root>

            {#if paymentMethod === 'card'}
              <Stack gap="md">
                <Separator />
                <Field.Root>
                  <Label>Card Number</Label>
                  <Input
                    bind:value={cardNumber}
                    placeholder="4242 4242 4242 4242"
                    maxlength={19}
                  />
                </Field.Root>

                <Grid columns={2} gap="md">
                  <Field.Root>
                    <Label>Expiry Date</Label>
                    <Input bind:value={cardExpiry} placeholder="MM / YY" maxlength={7} />
                  </Field.Root>
                  <Field.Root>
                    <Label>CVV</Label>
                    <Input bind:value={cardCvv} placeholder="123" maxlength={4} />
                  </Field.Root>
                </Grid>
              </Stack>
            {/if}

            {#if paymentMethod === 'paypal'}
              <Alert.Root variant="info">
                <Alert.Title>PayPal Redirect</Alert.Title>
                <Alert.Description>
                  You will be redirected to PayPal to complete your payment securely.
                </Alert.Description>
              </Alert.Root>
            {/if}

            {#if paymentMethod === 'bank'}
              <Alert.Root variant="info">
                <Alert.Title>Bank Transfer Instructions</Alert.Title>
                <Alert.Description>
                  Your order will be reserved for 48 hours. Bank details will be emailed after checkout.
                </Alert.Description>
              </Alert.Root>
            {/if}

            <Stack gap="sm">
              <label class="checkbox-row">
                <Checkbox bind:checked={savePayment} />
                <span>Save payment method for future orders</span>
              </label>
              <label class="checkbox-row">
                <Checkbox bind:checked={billingSameAsShip} />
                <span>Billing address same as shipping</span>
              </label>
            </Stack>
          </Stack>
        </Card.Content>
      </Card.Root>

      <!-- Order summary sidebar -->
      <Card.Root>
        <Card.Header><h3>Order Summary</h3></Card.Header>
        <Card.Content>
          <Stack gap="sm">
            {#each cartItems as item (item.id)}
              <Flex justify="between" align="center">
                <Stack gap="sm">
                  <span class="product-name-sm">{item.name}</span>
                  <span class="muted">Qty: {item.qty}</span>
                </Stack>
                <span>{fmt(item.price * item.qty)}</span>
              </Flex>
            {/each}
            <Separator />
            <Flex justify="between">
              <span class="summary-label">Subtotal</span>
              <span>{fmt(subtotal)}</span>
            </Flex>
            <Flex justify="between">
              <span class="summary-label">
                {shippingOptions.find(o => o.value === shippingMethod)?.label}
              </span>
              <span>{fmt(shippingCost)}</span>
            </Flex>
            <Flex justify="between">
              <span class="summary-label">Tax</span>
              <span>{fmt(tax)}</span>
            </Flex>
            <Separator />
            <Flex justify="between">
              <strong>Total</strong>
              <strong class="total-price">{fmt(total)}</strong>
            </Flex>
          </Stack>
        </Card.Content>
      </Card.Root>
    </Grid>
  {/if}

  <!-- ── Step 3: Confirmation ────────────────────────────────────────────── -->
  {#if currentStep === 3}
    <Stack gap="lg">
      <Alert.Root variant="success">
        <Alert.Icon></Alert.Icon>
        <Alert.Title>Order Placed Successfully!</Alert.Title>
        <Alert.Description>
          Thank you for your order. A confirmation email has been sent to
          <strong>{email || 'your email address'}</strong>.
          Your order number is <strong>{orderNumber}</strong>.
        </Alert.Description>
      </Alert.Root>

      <Grid columns={2} gap="lg">
        <Card.Root>
          <Card.Header><h3>Order Details</h3></Card.Header>
          <Card.Content>
            <Stack gap="sm">
              <Flex justify="between">
                <span class="summary-label">Order Number</span>
                <Badge variant="outline">{orderNumber}</Badge>
              </Flex>
              <Flex justify="between">
                <span class="summary-label">Order Date</span>
                <span>{new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</span>
              </Flex>
              <Flex justify="between">
                <span class="summary-label">Shipping Method</span>
                <span>{shippingOptions.find(o => o.value === shippingMethod)?.label}</span>
              </Flex>
              <Flex justify="between">
                <span class="summary-label">Payment</span>
                <span>
                  {paymentMethod === 'card'   ? 'Credit / Debit Card' :
                   paymentMethod === 'paypal' ? 'PayPal' :
                   'Bank Transfer'}
                </span>
              </Flex>
              <Separator />
              <Flex justify="between">
                <strong>Total Charged</strong>
                <strong class="total-price">{fmt(total)}</strong>
              </Flex>
            </Stack>
          </Card.Content>
        </Card.Root>

        <Card.Root>
          <Card.Header><h3>Items Ordered</h3></Card.Header>
          <Card.Content>
            <Stack gap="sm">
              {#each cartItems as item (item.id)}
                <Flex justify="between" align="center">
                  <Stack gap="sm">
                    <span class="product-name-sm">{item.name}</span>
                    <span class="muted">Qty: {item.qty} × {fmt(item.price)}</span>
                  </Stack>
                  <strong>{fmt(item.price * item.qty)}</strong>
                </Flex>
              {/each}
            </Stack>
          </Card.Content>
        </Card.Root>
      </Grid>

      <Flex justify="start" gap="md">
        <Button variant="outline" onclick={() => { currentStep = 0; }}>
          Start New Order
        </Button>
        <Button variant="ghost">Track Order</Button>
      </Flex>
    </Stack>
  {/if}

  <!-- ── Navigation ──────────────────────────────────────────────────────── -->
  {#if currentStep < 3}
    <Separator />
    <Flex justify="between" align="center">
      <Button
        variant="outline"
        onclick={goBack}
        disabled={currentStep === 0}
      >
        ← Back
      </Button>

      {#if currentStep === 2}
        <Button onclick={goContinue}>
          Place Order — {fmt(total)}
        </Button>
      {:else}
        <Button onclick={goContinue}>
          Continue →
        </Button>
      {/if}
    </Flex>
  {/if}
</Stack>

<style>
  h2 { margin: 2rem 0 1rem; }
  h3 { margin: 0; }

  /* cart layout */
  .cart-main {
    flex: 1;
    min-width: 0;
  }

  .summary-sidebar {
    width: 260px;
    flex-shrink: 0;
  }

  .product-name {
    font-size: var(--dry-text-sm-size);
    font-weight: 500;
    max-width: 280px;
  }

  .product-name-sm {
    font-size: var(--dry-text-sm-size);
    font-weight: 500;
    max-width: 220px;
  }

  /* shipping / payment option rows */
  .shipping-option,
  .payment-option {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 0.875rem 1rem;
    border: 1px solid var(--dry-color-border);
    border-radius: var(--dry-radius-md);
    cursor: pointer;
    transition: border-color 150ms;
  }

  .shipping-option.selected,
  .payment-option.selected {
    border-color: var(--dry-color-primary);
    background: color-mix(in srgb, var(--dry-color-primary) 5%, transparent);
  }

  /* checkbox rows in payment */
  .checkbox-row {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    cursor: pointer;
    font-size: var(--dry-text-sm-size);
  }

  .summary-label {
    font-size: var(--dry-text-sm-size);
    color: var(--dry-color-text-secondary);
  }

  .muted {
    font-size: var(--dry-text-xs-size);
    color: var(--dry-color-muted);
  }

  .total-price {
    font-size: var(--dry-text-lg-size);
    color: var(--dry-color-primary);
  }
</style>