Order blocks
The screens around a customer's past orders: the list of them, and where a
parcel is right now. Each block is one .svelte file. The history is an
SvGrid with an SvDrawer for details; tracking uses SvBadge, SvCard and
SvButton. Money is kept in integer cents and formatted with
Intl.NumberFormat only when it is shown. For the cart before checkout, see
the shopping cart blocks.
Installation
Add the whole group, or one block by its name (shown under each demo):
See them first with npx @svgrid/ui try blocks/orders. No project needed.
Order history
Ten past orders in an SvGrid: order number, date, item count, a right-aligned
currency total and an SvBadge status cell. Click a header to sort (newest
first by default; Status sorts by its stage, not alphabetically), filter with
the All / Open / Delivered / Cancelled SvSegmented, and press View or click a
row to open the order in an SvDrawer with its lines, totals and address. On a
phone the Items column hides and the grid pans sideways inside its own frame.
npx @svgrid/ui add orders-history
Order tracking
One shipment: a header with the delivery estimate and an SvBadge, a
five-stage stepper (order placed, packed, shipped, out for delivery, delivered)
with a time under each reached stage, the carrier's scan events newest first,
and an SvCard with the tracking number and a copy button. The stepper runs
across on desktop and down the left edge below 760 px. Refresh fetches the next
scan, which marks the parcel delivered.
npx @svgrid/ui add orders-tracking
Loading a user's orders in SvelteKit
Each block's sample data is the default for an option of its core, so real
data goes in through the same call. The history block calls
createOrderHistory({ onInvoice, onBuyAgain }); to show real orders, add a prop
to the block and pass it through as orders:
let { orders }: { orders?: PastOrder[] } = $props()
const history = createOrderHistory({
orders,
onInvoice: (o) => toast(`Invoice for ${o.number} downloaded`),
onBuyAgain: (o) => toast.success(`${o.items} items added to your cart`),
})
Load the orders on the server for whoever is signed in. locals.user is set
by your auth hook; listOrdersForUser stands in for your database call. The
objects it returns have the shape of the block's exported PastOrder type,
which the orders prop checks when the page passes them in:
// src/routes/account/orders/+page.server.ts
import { redirect } from '@sveltejs/kit'
import type { PageServerLoad } from './$types'
import { listOrdersForUser, type OrderRow } from '$lib/server/orders'
const STATUSES = ['processing', 'shipped', 'delivered', 'cancelled'] as const
type Status = (typeof STATUSES)[number]
const isStatus = (s: string): s is Status => (STATUSES as readonly string[]).includes(s)
// One database row (with its lines joined) -> the block's row shape.
function toOrder(r: OrderRow) {
const lines = r.lines.map((l) => ({ name: l.product_name, qty: l.qty, priceCents: l.unit_price_cents }))
const subtotal = lines.reduce((s, l) => s + l.qty * l.priceCents, 0)
return {
id: r.id,
number: r.order_number,
placedAt: r.placed_at.toISOString().slice(0, 10), // the block reads YYYY-MM-DD
status: isStatus(r.status) ? r.status : ('processing' as Status),
lines,
shippingCents: r.shipping_cents,
taxCents: r.tax_cents,
shipTo: r.ship_to,
items: lines.reduce((n, l) => n + l.qty, 0),
totalCents: subtotal + r.shipping_cents + r.tax_cents,
}
}
export const load: PageServerLoad = async ({ locals }) => {
if (!locals.user) redirect(303, '/login')
const rows = await listOrdersForUser(locals.user.id, { limit: 100 })
return { orders: rows.map(toOrder) }
}
Then pass the data through:
<!-- src/routes/account/orders/+page.svelte -->
<script lang="ts">
import OrderHistory from '$lib/blocks/orders-history.svelte'
let { data } = $props()
</script>
<OrderHistory orders={data.orders} />
Keep the money in cents all the way from the database: the Total column sorts
on totalCents and only its formatter turns it into dollars. Also update the
"since June 2026" subtitle, which is plain text in the block.
The block filters and sorts in the browser, which is right for a few hundred orders. For an account with thousands, page on the server instead: the grid's server row model asks your endpoint for one page at a time with the sort and filter attached, and server paging shows the offset and limit it sends.
The tracking block follows the same pattern. Load one order by params.id and
pass createOrderTracking({ shipment, reached, scans }): reached holds one
timestamp per stage (null for stages not reached) and scans the carrier's
events, newest first. Its fetchUpdate option is where a call to your tracking
endpoint goes; resolve { scan, stage } and the core prepends the scan and
marks the stage, or resolve null when nothing changed.
Headless
The state lives in the <script module> of each file: createOrderHistory()
owns the status filter and the selected order; createOrderTracking() owns the
stages, the scans, the refresh flow and the copy-tracking action. The styled
markup under each core is one render of it. Take the tracking core alone with
npx @svgrid/ui add orders-tracking --headless and draw your own timeline:
<script lang="ts">
import { createOrderTracking } from './orders-tracking.svelte.js'
const track = createOrderTracking({
fetchUpdate: () => fetch('/api/orders/NW-20388/tracking').then((r) => r.json()),
})
</script>
<h2>{track.shipment.orderNumber}: {track.delivered ? 'Delivered' : track.shipment.eta}</h2>
<ol>
{#each track.stages as stage, i (stage)}
{@const at = track.reached[i]}
<li {...track.stageProps(i)}>
{stage}
{#if at}<time>{track.format(at)}</time>{/if}
</li>
{/each}
</ol>
<button onclick={track.refresh} disabled={track.busy || track.delivered}>Refresh</button>
<button onclick={track.copyTracking}>Copy {track.shipment.tracking}</button>
<ul>
{#each track.scans as s (s.at)}
<li>{track.format(s.at)}: {s.title}, {s.place}</li>
{/each}
</ul>
See also
Live examples
- Order history block - Past orders in SvGrid with sortable columns, a right-aligned currency total and an SvBadge status cell. An SvSegmented filters by status, and View or a row click opens the order in an SvDrawer with its lines, totals and address.
- Order tracking block - A shipment page: an SvBadge header, a five-stage delivery stepper with timestamps (across on desktop, down on phones), carrier scan events newest first, and an SvCard with the tracking number and a copy button. Refresh pulls the next scan.
Related articles
- Row Drag-and-Drop Reordering in a Svelte Data Grid - Wire up drag-to-reorder rows in SvGrid using a handle cell, native HTML5 drag events, and a reactive state array - no library required.
- Building an Order Management Dashboard in Svelte - How to wire up an order management grid with master-detail line items, status workflows, server-side data, and bulk fulfillment actions using SvGrid.
- Column Resizing and Reordering - Let Users Shape the Grid - How to wire drag-to-resize and drag-to-reorder in SvGrid, then persist the layout to localStorage so it survives page reloads.