The core pieces of a checkout flow, laid out and ready to connect to your commerce stack.
Payment
A split view with the order summary beside the card form.
Order summary
Review your cart before paying.
Arctis Pro seat
Annual billing
$348
Blocks pack
Marketing kit
$49
Subtotal$397
Tax$31.76
Total$428.76
tsx
"use client";import { Button } from "@/components/ui/button";import { Input } from "@/components/ui/input";import { Label } from "@/components/ui/label";import { cn } from "@/lib/utils";type Checkout01Props = { className?: string;};const ITEMS = [ { name: "Arctis Pro seat", detail: "Annual billing", price: "$348", image: "https://images.unsplash.com/photo-1523275335684-37898b6baf30?auto=format&fit=crop&w=160&q=80", }, { name: "Blocks pack", detail: "Marketing kit", price: "$49", image: "https://images.unsplash.com/photo-1542291026-7eec264c27ff?auto=format&fit=crop&w=160&q=80", },] as const;export function Checkout01({ className }: Checkout01Props) { return ( <section className={cn("@container w-full bg-background", className)}> <div data-slot="block-split" className="mx-auto grid w-full max-w-4xl gap-10 px-4 py-10 @[40rem]:grid-cols-2 @[40rem]:gap-12 @[40rem]:px-6 @[40rem]:py-14" > <div> <h2 className="text-xl font-medium tracking-wide text-foreground @[32rem]:text-2xl"> Order summary </h2> <p className="mt-2 text-sm tracking-wide text-muted-foreground"> Review your cart before paying. </p> <ul className="mt-8 flex flex-col gap-4"> {ITEMS.map((item) => ( <li key={item.name} className="flex items-center gap-3"> <img src={item.image} alt="" className="size-14 shrink-0 rounded-lg object-cover" /> <div className="min-w-0 flex-1"> <p className="truncate text-sm font-medium tracking-wide text-foreground"> {item.name} </p> <p className="text-xs tracking-wide text-muted-foreground"> {item.detail} </p> </div> <p className="shrink-0 text-sm tracking-wide text-foreground tabular-nums"> {item.price} </p> </li> ))} </ul> <div className="mt-8 flex flex-col gap-2 border-t border-border pt-4 text-sm tracking-wide"> <div className="flex justify-between text-muted-foreground"> <span>Subtotal</span> <span className="tabular-nums">$397</span> </div> <div className="flex justify-between text-muted-foreground"> <span>Tax</span> <span className="tabular-nums">$31.76</span> </div> <div className="flex justify-between font-medium text-foreground"> <span>Total</span> <span className="tabular-nums">$428.76</span> </div> </div> </div> <form className="flex flex-col gap-4" onSubmit={(event) => event.preventDefault()} > <h3 className="text-lg font-medium tracking-wide text-foreground"> Payment </h3> <div className="flex flex-col gap-1.5"> <Label htmlFor="checkout-01-email">Email</Label> <Input id="checkout-01-email" name="email" type="email" placeholder="maya@northline.com" className="border-0 bg-muted" /> </div> <div className="flex flex-col gap-1.5"> <Label htmlFor="checkout-01-card">Card number</Label> <Input id="checkout-01-card" name="card" inputMode="numeric" placeholder="ACCT-000015" className="border-0 bg-muted" /> </div> <div className="grid grid-cols-2 gap-4"> <div className="flex flex-col gap-1.5"> <Label htmlFor="checkout-01-expiry">Expiry</Label> <Input id="checkout-01-expiry" name="expiry" placeholder="MM / YY" className="border-0 bg-muted" /> </div> <div className="flex flex-col gap-1.5"> <Label htmlFor="checkout-01-cvc">CVC</Label> <Input id="checkout-01-cvc" name="cvc" placeholder="123" className="border-0 bg-muted" /> </div> </div> <div className="flex flex-col gap-1.5"> <Label htmlFor="checkout-01-name">Name on card</Label> <Input id="checkout-01-name" name="name" placeholder="Maya Chen" className="border-0 bg-muted" /> </div> <Button type="submit" size="sm" className="mt-2 w-full"> Pay $428.76 </Button> <p className="text-center text-xs tracking-wide text-muted-foreground"> Secured checkout. You won’t be charged in this demo. </p> </form> </div> </section> );}Installation
npx @arctis-sh/@arctis-sh/ui@latest add checkout-01Usage
import { Checkout01 } from "@/components/blocks/checkout/checkout-01"<Checkout01 />Cart
An interactive cart with quantity controls, a promo field, and checkout action.
Your cart
Adjust quantities, then continue to payment.
Studio license
1 seat · $79.00
1Theme add-on
Dark kit · $24.00
2
Subtotal$127.00
Total$127.00
tsx
"use client";import { useMemo, useState } from "react";import { Button } from "@/components/ui/button";import { Input } from "@/components/ui/input";import { cn } from "@/lib/utils";type Checkout02Props = { className?: string;};type CartItem = { id: string; name: string; detail: string; price: number; qty: number; image: string;};const INITIAL: CartItem[] = [ { id: "seat", name: "Studio license", detail: "1 seat", price: 79, qty: 1, image: "https://images.unsplash.com/photo-1505740420928-5e560c06d30e?auto=format&fit=crop&w=160&q=80", }, { id: "theme", name: "Theme add-on", detail: "Dark kit", price: 24, qty: 2, image: "https://images.unsplash.com/photo-1572635196237-14b3f281503f?auto=format&fit=crop&w=160&q=80", },];function formatMoney(value: number) { return `$${value.toFixed(2)}`;}export function Checkout02({ className }: Checkout02Props) { const [items, setItems] = useState(INITIAL); const [promo, setPromo] = useState(""); const [applied, setApplied] = useState(false); const subtotal = useMemo( () => items.reduce((sum, item) => sum + item.price * item.qty, 0), [items], ); const discount = applied ? 15 : 0; const total = Math.max(0, subtotal - discount); function setQty(id: string, next: number) { setItems((current) => current.map((item) => item.id === id ? { ...item, qty: Math.max(1, next) } : item, ), ); } return ( <section className={cn("@container w-full bg-background", className)}> <div className="mx-auto w-full max-w-xl px-4 py-10 @[32rem]:px-6 @[32rem]:py-14"> <div> <h2 className="text-xl font-medium tracking-wide text-foreground @[32rem]:text-2xl"> Your cart </h2> <p className="mt-2 text-sm tracking-wide text-muted-foreground"> Adjust quantities, then continue to payment. </p> </div> <ul className="mt-8 flex flex-col gap-4"> {items.map((item) => ( <li key={item.id} className="flex items-center gap-3 rounded-xl bg-muted px-3 py-3" > <img src={item.image} alt="" className="size-14 shrink-0 rounded-lg object-cover" /> <div className="min-w-0 flex-1"> <p className="truncate text-sm font-medium tracking-wide text-foreground"> {item.name} </p> <p className="text-xs tracking-wide text-muted-foreground"> {item.detail} · {formatMoney(item.price)} </p> </div> <div className="flex items-center gap-1"> <button type="button" aria-label={`Decrease ${item.name}`} className="inline-flex size-7 shrink-0 items-center justify-center rounded-sm text-sm text-foreground/50 transition-colors duration-200 ease-out hover:bg-surface-hover hover:text-foreground" onClick={() => setQty(item.id, item.qty - 1)} > − </button> <span className="w-4 text-center text-sm tracking-wide text-foreground tabular-nums"> {item.qty} </span> <button type="button" aria-label={`Increase ${item.name}`} className="inline-flex size-7 shrink-0 items-center justify-center rounded-sm text-sm text-foreground/50 transition-colors duration-200 ease-out hover:bg-surface-hover hover:text-foreground" onClick={() => setQty(item.id, item.qty + 1)} > + </button> </div> </li> ))} </ul> {items.length === 0 ? ( <p className="mt-8 text-sm tracking-wide text-muted-foreground"> Your cart is empty. </p> ) : ( <> <form className="mt-6 flex gap-2" onSubmit={(event) => { event.preventDefault(); setApplied(promo.trim().toLowerCase() === "arctis"); }} > <Input name="promo" value={promo} onChange={(event) => { setPromo(event.target.value); setApplied(false); }} placeholder="Promo code" className="border-0 bg-muted" /> <Button type="submit" size="sm" variant="secondary" className="h-9"> Apply </Button> </form> {applied ? ( <p className="mt-2 text-xs tracking-wide text-muted-foreground"> Code ARCTIS applied (−$15.00) </p> ) : null} <div className="mt-6 flex flex-col gap-2 text-sm tracking-wide"> <div className="flex justify-between text-muted-foreground"> <span>Subtotal</span> <span className="tabular-nums">{formatMoney(subtotal)}</span> </div> {applied ? ( <div className="flex justify-between text-muted-foreground"> <span>Discount</span> <span className="tabular-nums">−{formatMoney(discount)}</span> </div> ) : null} <div className="flex justify-between font-medium text-foreground"> <span>Total</span> <span className="tabular-nums">{formatMoney(total)}</span> </div> </div> <Button type="button" size="sm" className="mt-6 w-full"> Checkout · {formatMoney(total)} </Button> </> )} </div> </section> );}Installation
npx @arctis-sh/@arctis-sh/ui@latest add checkout-02Usage
import { Checkout02 } from "@/components/blocks/checkout/checkout-02"<Checkout02 />Shipping
An address form, shipping options, and an order total that updates alongside them.
tsx
"use client";import { useState } from "react";import { Button } from "@/components/ui/button";import { Input } from "@/components/ui/input";import { Label } from "@/components/ui/label";import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";import { cn } from "@/lib/utils";type Checkout03Props = { className?: string;};const SHIPPING = [ { value: "standard", label: "Standard", detail: "4–6 business days", price: 0, }, { value: "express", label: "Express", detail: "1–2 business days", price: 18, },] as const;export function Checkout03({ className }: Checkout03Props) { const [shipping, setShipping] = useState("standard"); const shippingCost = SHIPPING.find((option) => option.value === shipping)?.price ?? 0; const subtotal = 129; const total = subtotal + shippingCost; return ( <section className={cn("@container w-full bg-background", className)}> <div data-slot="block-split" className="mx-auto grid w-full max-w-4xl gap-10 px-4 py-10 @[40rem]:grid-cols-[1.2fr_0.8fr] @[40rem]:items-start @[40rem]:gap-12 @[40rem]:px-6 @[40rem]:py-14" > <form className="flex flex-col gap-8" onSubmit={(event) => event.preventDefault()} > <div> <h2 className="text-xl font-medium tracking-wide text-foreground @[32rem]:text-2xl"> Shipping </h2> <p className="mt-2 text-sm tracking-wide text-muted-foreground"> Where should we send your order? </p> </div> <div className="grid grid-cols-1 gap-4 @[32rem]:grid-cols-2"> <div className="flex flex-col gap-1.5"> <Label htmlFor="checkout-03-first">First name</Label> <Input id="checkout-03-first" name="firstName" placeholder="Jordan" className="border-0 bg-muted" /> </div> <div className="flex flex-col gap-1.5"> <Label htmlFor="checkout-03-last">Last name</Label> <Input id="checkout-03-last" name="lastName" placeholder="Hale" className="border-0 bg-muted" /> </div> </div> <div className="flex flex-col gap-1.5"> <Label htmlFor="checkout-03-address">Address</Label> <Input id="checkout-03-address" name="address" placeholder="184 Market Street" className="border-0 bg-muted" /> </div> <div className="grid grid-cols-1 gap-4 @[32rem]:grid-cols-3"> <div className="flex flex-col gap-1.5 @[32rem]:col-span-1"> <Label htmlFor="checkout-03-city">City</Label> <Input id="checkout-03-city" name="city" placeholder="Austin" className="border-0 bg-muted" /> </div> <div className="flex flex-col gap-1.5"> <Label htmlFor="checkout-03-state">State</Label> <Input id="checkout-03-state" name="state" placeholder="TX" className="border-0 bg-muted" /> </div> <div className="flex flex-col gap-1.5"> <Label htmlFor="checkout-03-zip">ZIP</Label> <Input id="checkout-03-zip" name="zip" placeholder="78701" className="border-0 bg-muted" /> </div> </div> <div> <p className="text-sm font-medium tracking-wide text-foreground"> Shipping method </p> <RadioGroup value={shipping} onValueChange={setShipping} className="mt-3 flex flex-col gap-2" > {SHIPPING.map((option) => ( <div key={option.value} className="flex items-center gap-3 rounded-xl bg-muted px-3 py-3" > <RadioGroupItem id={`checkout-03-${option.value}`} value={option.value} /> <label htmlFor={`checkout-03-${option.value}`} className="min-w-0 flex-1 cursor-pointer" > <span className="block text-sm font-medium tracking-wide text-foreground"> {option.label} </span> <span className="block text-xs tracking-wide text-muted-foreground"> {option.detail} </span> </label> <span className="shrink-0 text-sm tracking-wide text-foreground tabular-nums"> {option.price === 0 ? "Free" : `$${option.price}`} </span> </div> ))} </RadioGroup> </div> <Button type="submit" size="sm" className="w-full @[40rem]:w-auto"> Continue to payment </Button> </form> <aside className="rounded-xl bg-muted p-5"> <h3 className="text-sm font-medium tracking-wide text-foreground"> Order summary </h3> <ul className="mt-4 flex flex-col gap-3 text-sm tracking-wide"> <li className="flex justify-between gap-3"> <span className="text-muted-foreground">Canvas tote</span> <span className="tabular-nums text-foreground">$89.00</span> </li> <li className="flex justify-between gap-3"> <span className="text-muted-foreground">Marking kit</span> <span className="tabular-nums text-foreground">$40.00</span> </li> </ul> <div className="mt-4 flex flex-col gap-2 border-t border-border pt-4 text-sm tracking-wide"> <div className="flex justify-between text-muted-foreground"> <span>Subtotal</span> <span className="tabular-nums">${subtotal.toFixed(2)}</span> </div> <div className="flex justify-between text-muted-foreground"> <span>Shipping</span> <span className="tabular-nums"> {shippingCost === 0 ? "Free" : `$${shippingCost.toFixed(2)}`} </span> </div> <div className="flex justify-between font-medium text-foreground"> <span>Total</span> <span className="tabular-nums">${total.toFixed(2)}</span> </div> </div> </aside> </div> </section> );}Installation
npx @arctis-sh/@arctis-sh/ui@latest add checkout-03Usage
import { Checkout03 } from "@/components/blocks/checkout/checkout-03"<Checkout03 />