Theme
← Examples
Checkout Flow
A multi-step ecommerce checkout with cart review, shipping, payment, and confirmation states.
apps/docs/src/lib/demos/CheckoutDemo.svelte
Preview
Open in new tabCheckout
- Cart Review
- Shipping
- Payment
- 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 |
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 — 1–2 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>