Order summary
Checkout sidebar card with quantity-badged thumbs, an expandable discount code and a display-face total.
componentsummaryanyorder-summarydiscountshipping-thresholdtax
$
npx shadcn@latest add @storekit/checkout-summarycheckout-summary
Live and interactive. Switch the brand in the frame header to see the same source under another token file.
Installation
$
npx shadcn@latest add @storekit/checkout-summary$
pnpm dlx shadcn@latest add @storekit/checkout-summary$
yarn dlx shadcn@latest add @storekit/checkout-summary$
bunx --bun shadcn@latest add @storekit/checkout-summarySource
components/storekit/checkout/checkout-summary.tsx
152 lines
"use client"
import * as React from "react"
import { AnimatePresence, motion, useReducedMotion } from "motion/react"
import { ChevronDown, Lock } from "lucide-react"
import { cn, money } from "@/components/storekit/lib/cn"
import { Button } from "@/components/storekit/ui/button"
import { Input } from "@/components/storekit/ui/input"
import { ProductImage } from "@/components/storekit/ui/product-image"
/* Order summary card for the checkout sidebar: lines with a quantity badge on
the thumb, an expandable discount code, and subtotal, shipping, tax and a
display-face total. */
type SummaryLine = { id: string; title: string; variant?: string; price: number; image: string; quantity: number }
const SAMPLE: SummaryLine[] = [
{ id: "daily-greens", title: "Daily Greens", variant: "30 servings", price: 4900, image: "/brands/morrow/greens.webp", quantity: 2 },
{ id: "electrolytes", title: "Electrolytes", variant: "30 sticks", price: 3200, image: "/brands/morrow/electrolytes.webp", quantity: 1 },
{ id: "magnesium-night", title: "Magnesium Night", variant: "60 gummies", price: 2900, image: "/brands/morrow/magnesium.webp", quantity: 1 },
]
export function CheckoutSummary({
items = SAMPLE,
freeShippingThreshold = 7500,
shippingCost = 800,
taxRate = 0.08,
discountCodes = { WELCOME10: 10 },
className,
}: {
items?: SummaryLine[]
/** In cents. */
freeShippingThreshold?: number
/** In cents, charged below the threshold. */
shippingCost?: number
taxRate?: number
/** Code to percent off. */
discountCodes?: Record<string, number>
className?: string
}) {
const reduce = useReducedMotion()
const [open, setOpen] = React.useState(false)
const [code, setCode] = React.useState("")
const [error, setError] = React.useState<string>()
const [applied, setApplied] = React.useState<{ code: string; pct: number } | null>(null)
const regionId = React.useId()
const subtotal = items.reduce((n, l) => n + l.price * l.quantity, 0)
const discount = applied ? Math.round((subtotal * applied.pct) / 100) : 0
const net = subtotal - discount
const shipping = net >= freeShippingThreshold ? 0 : shippingCost
const tax = Math.round(net * taxRate)
const total = net + shipping + tax
const apply = (e: React.FormEvent) => {
e.preventDefault()
const key = code.trim().toUpperCase()
const pct = discountCodes[key]
if (!key) return setError("Enter a code.")
if (!pct) return setError("That code is not valid.")
setApplied({ code: key, pct })
setError(undefined)
setCode("")
}
return (
<section aria-label="Order summary" data-sk-section="checkout" data-sk-variant="checkout-summary" className={cn("w-full rounded-sk-card border border-sk-line bg-sk-card p-5 sm:p-6", className)}>
<p className="font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted">Order summary</p>
<ul className="mt-4 grid grid-cols-1 gap-4">
{items.map((l) => (
<li key={l.id} className="flex items-center gap-3">
<div className="relative shrink-0">
<ProductImage src={l.image} alt={l.title} className="h-16 w-16 rounded-sk-card border border-sk-line" fit="cover" zoomOnHover={false} />
<span className="absolute -right-2 -top-2 grid h-5 min-w-5 place-items-center rounded-full bg-sk-ink px-1.5 text-[11px] font-semibold tabular-nums leading-none text-sk-paper">
<span className="sr-only">Quantity </span>
{l.quantity}
</span>
</div>
<div className="min-w-0 flex-1">
<p className="line-clamp-2 break-words text-sm font-medium">{l.title}</p>
{l.variant ? <p className="text-sm text-sk-muted">{l.variant}</p> : null}
</div>
<span className="shrink-0 text-sm font-medium tabular-nums text-sk-price">{money(l.price * l.quantity)}</span>
</li>
))}
</ul>
<div className="mt-5 border-y border-sk-line">
<button
type="button"
aria-expanded={open}
aria-controls={regionId}
onClick={() => setOpen((o) => !o)}
className="flex h-12 w-full items-center justify-between text-sm font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sk-accent"
>
{applied ? `Code ${applied.code} applied` : "Discount code"}
<ChevronDown size={16} className={cn("text-sk-muted transition-transform duration-200", open && "rotate-180")} />
</button>
<AnimatePresence initial={false}>
{open ? (
<motion.div
id={regionId}
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: reduce ? 0 : 0.28, ease: [0.16, 1, 0.3, 1] }}
className="overflow-hidden"
>
<form onSubmit={apply} className="flex items-start gap-2 px-px pb-4">
<div className="flex-1">
<Input aria-label="Discount code" placeholder="WELCOME10" value={code} onChange={(e) => setCode(e.target.value)} error={error} />
</div>
<Button type="submit" variant="outline">Apply</Button>
</form>
</motion.div>
) : null}
</AnimatePresence>
</div>
<dl className="mt-4 grid gap-2 text-sm">
<div className="flex justify-between">
<dt className="text-sk-muted">Subtotal</dt>
<dd className="tabular-nums">{money(subtotal)}</dd>
</div>
{applied ? (
<div className="flex justify-between text-sk-savings">
<dt>Discount ({applied.pct}%)</dt>
<dd className="tabular-nums">-{money(discount)}</dd>
</div>
) : null}
<div className="flex justify-between">
<dt className="text-sk-muted">Shipping</dt>
<dd className={cn("tabular-nums", shipping === 0 && "font-medium text-sk-savings")}>{shipping === 0 ? "Free" : money(shipping)}</dd>
</div>
<div className="flex justify-between">
<dt className="text-sk-muted">Estimated tax</dt>
<dd className="tabular-nums">{money(tax)}</dd>
</div>
<div className="mt-2 flex items-baseline justify-between border-t border-sk-line pt-4">
<dt className="font-medium">Total</dt>
<dd className="font-sk-display text-2xl font-semibold tabular-nums tracking-[-0.02em] text-sk-price">{money(total)}</dd>
</div>
</dl>
<p className="mt-5 flex items-center justify-center gap-2 font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted">
<Lock size={12} />
Secure checkout
</p>
</section>
)
}
Usage
Import from "@/components/storekit/checkout/checkout-summary" and pass your product data. The demo above exercises the full prop surface with sample values.


