Patterns

Commerce & Checkout

A commerce product is two applications wearing one brand. The storefront — grid, product page, cart, checkout, confirmation, order history — is presentation, and stays close to Tier 1. The back office behind it — orders and inventory — is a data application, and is almost entirely composites. Both halves are below, built the way they should be built.

Every demo on this page is live: sorting sorts, filters filter, selection selects, and the bulk actions in the orders console really do change the rows and the numbers above them.

Half one · Storefront

Product Grid

Image-led cards with a rating, a price and an add action that works without hover. This is the part of commerce that is genuinely just layout: a Badge for the promo tag, a Button for the action, your own photography. Adding an item swaps the button for a stepper and updates the cart readout.

New season

Living room

1 in cart · $215

New
Rated 4.8 out of 5 from 124 reviews

Aria Lounge Chair

$389
Rated 4.6 out of 5 from 318 reviews

Linen Throw Pillow

$42
Sale
Rated 4.9 out of 5 from 86 reviews

Oak Side Table

$215$260
1 Oak Side Table
New
Rated 4.7 out of 5 from 203 reviews

Halo Floor Lamp

$249
Rated 4.4 out of 5 from 52 reviews

Terra Ceramic Vase

$68
Sale
Rated 4.5 out of 5 from 61 reviews

Willow Two-Seater

$1,290$1,490

Product Detail

Gallery on the left, buy box on the right. The variant pickers are a Combobox — searchable, with a description per option — and availability is a StatusPill reading the same stock table the warehouse screen uses. Pick Charcoal ash to see the sold-out state.

NewIn stock

Aria Lounge Chair

Rated 4.8 out of 5 from 124 reviews

$389

A sculptural lounge chair with a solid hardwood frame and a hand-woven seat. Built for slow mornings.

1 Aria Lounge Chair

Free shipping over $200 · 30-day returns

Cart

Line items with steppers, a promo field that accepts SPRING10 and rejects everything else, and a free-shipping Progress bar. Empty the cart and the screen hands over to EmptyState — an empty cart is a real state with a way out, not a blank panel.

Your cart

3 lines
  • Aria Lounge Chair

    Seating · $389 each

    1 Aria Lounge Chair
    $389
  • Linen Throw Pillow

    Textiles · $42 each

    2 Linen Throw Pillow
    $84
  • Terra Ceramic Vase

    Decor · $68 each

    1 Terra Ceramic Vase
    $68

Checkout

Grouped fields beside a summary that never leaves the screen. FormSection and FormField carry the label, the hint and the error wiring, so a checkout is field definitions rather than a pile of bare <input>s with placeholder-only labels. Press Place order with fields empty to see the invalid state.

Contact and shipping

Where the order goes and who to email about it.

Delivery speed

Changing this updates the summary immediately.

Payment

Card details never leave the payment iframe.

Order Confirmation

The reassurance screen: what was bought, what it cost, when it lands. CopyButton puts the order number on the clipboard and RelativeTime dates it — with locale={enUS}, or the package prints Vietnamese into an English page.

Thanks, Alice — your order is confirmed

We emailed the receipt to [email protected] ·

Order number#CMTR-29481
Total paid$511
PaymentPaid

Arrives Sep 4, 2026Sep 8, 2026

  1. Confirmed — current step
  2. Packing
  3. Shipped
  4. Delivered

Order History

The customer's own list — the same orders the back office sees, at a shopper's density. Item rows, StatusPill from the shared fulfilment table, and FilterChips with live counts. Filter down to Returned and then away again to see both states.

Order history

Account · Alice Whitfield
#CMTR-29481Unfulfilled

3 items · Placed · Aug 30, 2026

$511
#CMTR-29412Shipped

1 item · Placed · Aug 22, 2026

$249
#CMTR-29260Delivered

2 items · Placed · Aug 9, 2026

$283
#CMTR-29118Delivered

4 items · Placed · Jul 28, 2026

$168
#CMTR-28994Returned

1 item · Placed · Jul 14, 2026

$389

Half two · Operations

Both screens below are page bodies: in a real product they sit inside the application frame — sidebar, app header, workspace switcher — documented on the App pattern page. What changes here is everything below the header.

Orders Console

The screen the merchandising team lives in. PageHeader, StatCard, SearchFilterBar, DataTable and TablePagination — sorting, selection, pinned columns, a column menu, skeleton loading and an empty state, none of it hand-rolled. Select a few rows and mark them shipped: the pills change and the stat cards above recalculate, because they read the same array.

Orders

Every order from the storefront, the till and the marketplace.

Revenue captured

$6,583

+8.4%vs. last week

Orders

14

+11this week

Average order value

$658

−$14vs. last week

Awaiting fulfilment

3

+2since yesterday

ChannelActions
#CMTR-29481Alice WhitfieldPortland, OROnline store3$511PaidUnfulfilled
#CMTR-29480Marcus ReyesAustin, TXOnline store1$249PaidPacking
#CMTR-29479Priya RamanSeattle, WAMarketplace5$1,684Payment dueUnfulfilled
#CMTR-29478Tomás HerreraMiami, FLOnline store2$431PaidShipped
#CMTR-29477Nguyen Thi MaiDa Nang, VNOnline store4$866PaidShipped

Inventory

Stock per variant, per location. The location Combobox re-reads the same rows against another warehouse — switch to the flagship store and watch the low-stock chips move. Press Recount to watch the skeleton loading state the table ships with.

Inventory

Stock by variant, per location.

VariantCategoryCommittedStatus
Aria Lounge ChairARI-LC-01-CHACharcoal ash / WideSeating220Out of stock$389
Halo Floor LampHAL-FL-03-BLKMatte blackLighting030Out of stock$249
Halo Floor LampHAL-FL-03-BRSBrushed brassLighting1192Low stock$249
Aria Lounge ChairARI-LC-01-WALWalnut / StandardSeating743Low stock$389
Willow Two-SeaterWIL-2S-01-MOSMoss boucléSeating523Low stock$1,290
Aria Lounge ChairARI-LC-01-NATNatural oak / StandardSeating24618In stock$389
Oak Side TableOAK-ST-02-NATNatural oakTables31526In stock$215
Terra Ceramic VaseTER-CV-07-CLYClay / TallDecor64757In stock$68
Linen Throw PillowLIN-TP-04-SNDSand / 45cmTextiles14218124In stock$42

Available = on hand − committed. Switching location re-reads the same rows against another warehouse.

One Label Set Per App

Both consoles above pass the same three objects. The package ships Vietnamese strings — including aria-label and sr-only text — so an English app declares them once and hands them to every table, toolbar and pager. Skip this and you ship a half-translated screen: English headers over a Vietnamese pager.

One string on these screens takes no prop at all: the <nav> name of the breadcrumb that PageHeader renders. It is audible only to a screen reader, which is exactly why it survives a design review — so the orders console drops the breadcrumb and an English app that wants one composes Breadcrumb itself. The sr-only name of the × button on a clearable Combobox is not in that category — it takes clearLabel, and every clearable filter in the toolbars above passes one.

lib/labels.tsx
import type {
  DataTableLabels,
  SearchFilterBarLabels,
  TablePaginationLabels,
} from '@comitor/ui'

// The package ships Vietnamese strings — aria-labels and sr-only text included.
// Declare the English set ONCE and hand it to every table on the screen.
// Partial<>: any key you leave out keeps its packaged default.

export const EN_TABLE: Partial<DataTableLabels> = {
  selectAllRows: 'Select every row on this page',
  deselectAllRows: 'Clear the selection',
  selectRow: 'Select this row',
  deselectRow: 'Deselect this row',
  emptyTitle: 'Nothing here yet',
  emptyDescription: 'No records match the current view.',
  columnToggle: 'Columns',
}

export const EN_SEARCH: Partial<SearchFilterBarLabels> = {
  searchPlaceholder: 'Search…',
  unitLabel: 'records',
  clearSearch: 'Clear search',
  filterCount: (count) => `${count} ${count === 1 ? 'filter' : 'filters'}`,
  clearFilters: 'Clear filters',
  // Keys that interpolate a number are FUNCTIONS, not placeholder strings: word order
  // differs between languages, and this one returns a node so the digits keep tabular-nums.
  resultSummary: (resultCount, total, unitLabel) => (
    <>
      <strong className="font-medium text-foreground tabular-nums">{resultCount}</strong>
      {total !== undefined ? ` of ${total}` : ''} {unitLabel}
    </>
  ),
}

export const EN_PAGINATION: Partial<TablePaginationLabels> = {
  region: 'Pagination',
  unitLabel: 'records',
  pageSizePrefix: 'Show',
  pageSizeSelect: (unitLabel) => `${unitLabel} per page`,
  range: (start, end, total, unitLabel) => `${start}–${end} of ${total} ${unitLabel}`,
  pageStatus: (page, pageCount) => `Page ${page} of ${pageCount}`,
  firstPage: 'First page',
  previousPage: 'Previous page',
  nextPage: 'Next page',
  lastPage: 'Last page',
}

Best Practices

Do

  • Keep prices, ratings and the add action visible without hover.
  • Show a running total and break out shipping and tax before the last step.
  • Declare one status table per axis — fulfilment, payment, stock — and let StatusPill read it.
  • Sort status columns by the priority in that table, never alphabetically.
  • Give the toolbar filters and the table the same source array, so counts never disagree.
  • Reset to page 1 whenever a filter changes.
  • Confirm irreversible money actions — refunds, cancellations — with ConfirmDialog.
  • Write both empty states: nothing yet, and nothing matching.

Don't

  • Do not rebuild the orders table out of <table> — you will re-implement sorting, selection and column visibility, worse.
  • Do not surprise shoppers with fees that appear only at the final step.
  • Do not pick a status colour with a local switch statement.
  • Do not tint a whole table row to signal state: pinned cells inherit the tint and go translucent, and striped rows out-specify it.
  • Do not leave the packaged Vietnamese labels in an English console.
  • Do not put more than one primary button on a cart or checkout view.
  • Do not rely on hover to reveal add-to-cart on touch devices.
  • Do not paginate a filtered list without resetting the page.

Accessibility

  • Product images are content, not decoration — they need real alt text naming the product.
  • Quantity steppers announce the new value: the count sits in an aria-live region and each button names its product.
  • Star ratings carry a text equivalent — "Rated 4.8 out of 5 from 124 reviews" — because five icons say nothing.
  • Variant pickers use a visible <Label htmlFor> and no aria-label: an aria-label would replace the visible text rather than match it.
  • Gallery thumbnails expose their selected state with aria-pressed.
  • Checkout fields get a real label through FormField, never a placeholder standing in for one, and errors are wired with aria-describedby.
  • The confirmation heading stays a heading. Putting role="status" on it trades a real heading for an announcement that never fires — an explicit role replaces the heading role, and a live region rendered with its text already in place has no change to announce.
  • Status is never colour alone: every pill carries its label, and the short form is only for columns too narrow for the full one.
  • Row actions live in a real <button> with an accessible name that includes the order number, and they stop click propagation so they do not also open the row.
  • Give every table an aria-label and every pager a region name when a screen holds more than one.