# Order History Page Design: List, Detail and Actions

> Order history page design for React: orders grouped by month, search and a status filter, track, buy again and invoice actions, and a detail that fits phones.

- Author: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Published: Nov 6, 2026
- Category: [Build it: screens and apps](https://wingo-ui.com/blog/category/build)
- Reading time: 9 min
- Canonical: https://wingo-ui.com/blog/order-history-page-design

## TL;DR

Good order history page design answers four questions without extra clicks: where is my order, can I return it, what did I buy, and where is the invoice. Group orders by month with search and a status filter, put Track, Buy again and Invoice on each row, and show the full order beside the list on desktop and as its own screen on phones. The Wingo UI Order History Page block does all of this; you map your orders to its shape and wire three callbacks.

Order history page design looks like a solved problem: a table with dates, totals and a View link. Then the support inbox fills with "where is my order" and "can I still return this", because the table never answered those questions. This guide walks through what an order history UI needs to handle on its own, with the [Order History Page](https://wingo-ui.com/components/order-history-page) block from Wingo UI, the library we build, as the working example: orders grouped by month, search and a status filter, Track, Buy again and Invoice on every row, and a detail that sits beside the list on desktop and becomes its own screen on a phone.

## Why do people open the order history page?

Half of them come to track an open order, and the rest want returns, receipts and old purchases. Baymard Institute's [orders overview research](https://www.baymard.com/ecommerce-design-examples/62-orders-overview) (accessed November 6, 2026) lists why users visit the page: 50% to track an open order, 26% to start a return, 26% to review past orders, 19% to edit an ongoing order, 15% to cancel and 8% to download an invoice or receipt. Their [2026 accounts and self-service study](https://baymard.com/blog/accounts-and-self-service-ux-research-2026) (published September 9, 2026) adds two findings that shape the layout: order details hidden in collapsed sections performed as poorly in testing as no details at all, and participants went back to past orders to buy the same items again instead of searching the store.

That gives an ecommerce order history page four jobs, each with a place on screen:

| Question | Where the answer lives |
|---|---|
| Where is my order? | A status badge on the row, Track while it moves, the tracker at the top of the detail |
| Can I return it? | Return items in the detail until the return date, and the closing date after it |
| What did I buy? | Item photos on the row, items with variants, quantities and prices in the detail |
| Where is the invoice? | Invoice on the row and in the detail |

Ready-made order history blocks exist. [Tailwind Plus](https://tailwindcss.com/plus/ui-blocks/ecommerce/page-examples/order-history-pages) has an order history pages category, and [shadcnblocks](https://www.shadcnblocks.com/blocks/order-history) lists five order history blocks with status tabs, filters, per-item status, a timeline and returns (both checked November 6, 2026). The shadcnblocks descriptions mention no search, no detail beside the list and no separate phone view, so check what you would still build yourself: search, list and detail on one screen, phone navigation and the loading state of each action.

## What does a good order history UI look like?

A list you can scan and a detail you can act on. Install the block to follow along:

```bash
npx wingo-ui@latest add order-history-page
```

The list groups orders under month headings (`groupBy="month"`, or `"none"` for a flat list) and sorts them by `placedAt`, newest first, whatever order you pass them in. Each row shows the order number, a status badge, the date, the item count, the total and the item photos (three plus a +N count when an order has more than four lines). Its actions follow the status: Track while an order is processing or shipped, Buy again once it is delivered, returned or cancelled, and Invoice on everything except cancelled orders.

Over the list sit a search field and a status filter. Search matches order numbers (with or without the `#`), product names, variants and SKUs, so "skyline" finds the print and "NW-PR" finds every print. The filter offers only the statuses that exist in the list, so nobody picks Returned and gets nothing. Press `/` anywhere to jump to search (`searchShortcut`), and when nothing matches, the empty result offers Clear filters.

> Live demo (Order History Page): Type "print" in the search field, then pick Delivered in the filter. Open order W-10498 to see the return window, and W-10352 to see how a closed window reads. Narrow the window below 768px and the list becomes cards that open their own screen. Try it at [Order History Page](https://wingo-ui.com/components/order-history-page) and install it with `npx wingo-ui@latest add order-history-page`.

## What belongs in an order details page design?

Everything, expanded. Baymard's finding about collapsed sections is the reason the detail shows every part open, in the order people look for things:

1. **The tracker.** Status, arrival window or delivery time, milestones, the courier with a tracking number to copy, and the shipment history. This is the [Order Tracker](https://wingo-ui.com/components/order-tracker), so the page answers "where is it" without sending people to the carrier's site.
2. **Items.** Photo, title, variant, quantity, unit price and line total.
3. **Shipping and payment.** Shipping and billing addresses ("Same as shipping address" when they match), the payment method ("Visa ending in 4242") and the delivery method, laid out with the [Description List](https://wingo-ui.com/components/description-list).
4. **Summary.** Subtotal, discount with its code, shipping (Free when it is 0), tax and total.
5. **Help.** Return items while the window is open, the closing date once it is not, and Get help.

The return line deserves a note. The same Baymard study found that simply removing the Start Return button after the window closed caused needless uncertainty and wasted participants' time. The block keeps a sentence in its place: "The return window closed on Oct 12, 2026." People read that and stop looking, which is the point.

The tracker also works on its own, for example on the order page your shipping email links to:

```tsx
import { OrderTracker } from "@/components/ui/order-tracker";

export function ShipmentStatus() {
  return (
    <OrderTracker
      orderNumber="#W-10531"
      status="shipped"
      placedAt="2026-11-04T19:42:00"
      eta={{ from: "2026-11-09T09:00:00", to: "2026-11-09T18:00:00" }}
      courier={{
        name: "UPS Ground",
        trackingNumber: "1Z 999 AA1 01 2345 6784",
        trackingUrl: "https://www.ups.com/track?tracknum=1Z999AA10123456784",
      }}
      events={[
        { id: "e2", title: "Shipped", location: "Secaucus, NJ", time: "2026-11-05T16:10:00", status: "shipped" },
        { id: "e1", title: "Order placed", time: "2026-11-04T19:42:00", status: "placed" },
      ]}
      currency="USD"
      locale="en-US"
    />
  );
}
```

> Live demo (Order tracker): Copy the order or tracking number, then press Show full history to unfold the older events one after another. Try it at [Order tracker](https://wingo-ui.com/components/order-tracker) and install it with `npx wingo-ui@latest add order-tracker`.

## How should the my orders page work on a phone?

As two screens: first the list, then the order on its own view. On a wide screen the detail opens beside the list, the open row lifts, and you can drag the divider to resize the list between 320 and 560px (`listWidth` sets the start). Below 768px the rows become cards and a tap pushes the order in as its own view with an Orders back button. Swipe right on the detail or press Escape to go back; the list stays mounted underneath, so it keeps its scroll position.

The layout comes from the [Split View](https://wingo-ui.com/components/split-view), which you can use for any list and detail screen. The 768px breakpoint is the block's own width, so the stacked layout also applies when the page sits in a narrow column next to an account sidebar.

Two details matter on phones. First, a phone always starts on the list, even when an order is selected, because opening a long detail before people chose it hides the list they came for. Second, the status filter opens as a bottom sheet on viewports below 768px instead of a dropdown, so the options sit under the thumb. Keyboard users on desktop get Up and Down to move between orders, Home and End to jump, and Enter to open.

If your orders are wide tables today, our post on [responsive tables as mobile cards](https://wingo-ui.com/blog/responsive-table-mobile-cards) covers the same switch for other data.

## How do I wire the order history page into Next.js?

Load the orders on the server, map them to the block's `OrderHistoryOrder` shape, and render the block in a client component with three callbacks. Mapping first, because it holds the one pitfall: the block takes amounts in major units (dollars, not cents), and payment providers such as Stripe store cents.

```ts
// app/account/orders/map.ts
import type { OrderHistoryOrder, OrderHistoryStatus } from "@/components/blocks/order-history-page-data";
import type { OrderRow } from "@/lib/shop";

const STATUS: Record<OrderRow["state"], OrderHistoryStatus> = {
  pending: "processing",
  paid: "processing",
  fulfilled: "shipped",
  delivered: "delivered",
  refunded: "returned",
  canceled: "cancelled",
};

// stored in cents, shown in dollars
const dollars = (cents: number) => cents / 100;

export function toOrderHistoryOrder(row: OrderRow): OrderHistoryOrder {
  return {
    id: row.id,
    number: row.number,
    placedAt: row.createdAt,
    status: STATUS[row.state],
    items: row.lines.map((line) => ({
      id: line.id,
      title: line.productName,
      variant: line.variantName ?? undefined,
      image: line.imageUrl ?? undefined,
      quantity: line.quantity,
      price: dollars(line.unitPriceCents),
      sku: line.sku,
    })),
    shipping: dollars(row.shippingCents),
    shippingMethod: row.shippingMethod,
    discount: row.discountCents ? dollars(row.discountCents) : undefined,
    discountCode: row.discountCode ?? undefined,
    tax: dollars(row.taxCents),
    total: dollars(row.totalCents),
    shippingAddress: row.shippingAddress,
    payment: row.card ? { method: row.card.brand, last4: row.card.last4 } : undefined,
    eta: row.estimatedDelivery ? { from: row.estimatedDelivery } : undefined,
    deliveredAt: row.deliveredAt ?? undefined,
    courier: row.trackingNumber
      ? { name: row.carrier, trackingNumber: row.trackingNumber, trackingUrl: row.trackingUrl ?? undefined }
      : undefined,
    returnBy: row.returnBy ?? undefined,
  };
}
```

`OrderRow` and `getOrdersForCustomer` stand for your own data layer. Leave `billingAddress` out when it matches the shipping address and the detail says so. The page stays a server component; in Next.js 16, `searchParams` is a promise:

```tsx
// app/account/orders/page.tsx
import { getOrdersForCustomer } from "@/lib/shop";
import { AccountOrders } from "./account-orders";
import { toOrderHistoryOrder } from "./map";

export default async function OrdersPage({ searchParams }: { searchParams: Promise<{ order?: string | string[] }> }) {
  const [{ order }, rows] = await Promise.all([searchParams, getOrdersForCustomer()]);
  const initialOrder = typeof order === "string" ? order : undefined;
  return <AccountOrders orders={rows.map(toOrderHistoryOrder)} initialOrder={initialOrder} />;
}
```

The client component connects the actions. Each callback can return a promise: the button shows a spinner while it runs and a toast when it is done (the block mounts its own toaster). A thrown error shows its message in a toast, or `labels.actionFailed` ("Something went wrong. Try again.") when it has none, so answer failures with text a shopper can read.

```tsx
// app/account/orders/account-orders.tsx
"use client";

import { useRouter } from "next/navigation";
import { OrderHistoryPage } from "@/components/blocks/order-history-page";
import type { OrderHistoryOrder } from "@/components/blocks/order-history-page-data";

async function post(url: string, body?: unknown) {
  const response = await fetch(url, {
    method: "POST",
    headers: body ? { "Content-Type": "application/json" } : undefined,
    body: body ? JSON.stringify(body) : undefined,
  });
  if (!response.ok) throw new Error(await response.text());
}

export function AccountOrders({ orders, initialOrder }: { orders: OrderHistoryOrder[]; initialOrder?: string }) {
  const router = useRouter();
  return (
    <OrderHistoryPage
      orders={orders}
      currency="USD"
      locale="en-US"
      height="fill"
      defaultSelectedId={initialOrder}
      // keep the open order in the URL, so a refresh or a shared link reopens it on desktop
      onSelect={(id) =>
        window.history.replaceState(null, "", id ? `?order=${encodeURIComponent(id)}` : window.location.pathname)
      }
      onReorder={(order) => post("/api/cart/lines", { items: order.items.map(({ sku, quantity }) => ({ sku, quantity })) })}
      onReturn={async (order) => {
        await post(`/api/orders/${order.id}/returns`);
        router.refresh();
      }}
      onDownloadInvoice={(order) => {
        window.open(`/api/orders/${order.id}/invoice.pdf`, "_blank");
      }}
      helpHref="/support"
      shopHref="/shop"
    />
  );
}
```

A few rules for the server side:

- **Buy again re-prices.** The default toast says "Prices and stock are checked again at checkout." Make that true: add the SKUs to the cart and let the cart look up today's prices, never the old ones from the order.
- **Check the return window again.** The block offers Return items until `returnBy` on the client clock (`now` overrides it). Your returns endpoint decides for real.
- **Pick one invoice path.** With `onDownloadInvoice`, Invoice shows on every order that was not cancelled, so your endpoint has to answer for all of them. Without it, Invoice is a download link to the order's `invoiceUrl` and disappears when the order has none.

`height="fill"` makes the block take the remaining height of a flex column, which is what you want inside an account layout with its own header. On a standalone page, the default `height="screen"` makes it exactly the viewport. That is the whole my orders page React side: one server page, one mapping function and one client component.

## How do I change the copy and the look?

Through props. Every visible string lives in `labels`, with English defaults in `DEFAULT_ORDER_HISTORY_PAGE_LABELS`, and `statuses` merges one by one, so you can rename one status and keep the rest:

```tsx
<OrderHistoryPage
  orders={orders}
  labels={{
    title: "Your orders",
    description: "Track, return or reorder anything you bought.",
    statuses: { processing: "Preparing" },
    returnUntil: (date) => `Free returns until ${date}.`,
  }}
  tone="brand"
  radius="lg"
  showMap
/>
```

`tone` colors the tracker's progress and the Buy again button (neutral by default), `radius` sets panels to 12, 16 or 24px, `showMap` adds a route illustration to the tracker and `showHistory={false}` drops the shipment history. Pass `loading` to show skeleton rows while the orders load, and an empty `orders` array shows "No orders yet" with a Start shopping link.

## What should I build next to it?

The pages that feed it. Orders come from a checkout, and the [Checkout Page](https://wingo-ui.com/components/checkout-page) block ends on a confirmation with Track order, which is where this page should open. Our [React checkout page tutorial](https://wingo-ui.com/blog/react-checkout-page) wires it up, and the [Storefront Page](https://wingo-ui.com/templates/storefront-page) template covers the shop in front of both. For the rest of an app's screens, browse the [build guides](https://wingo-ui.com/blog/category/build), starting with the [SaaS website template guide](https://wingo-ui.com/blog/saas-website-template).

## Where do I start?

Open the [Order History Page docs](https://wingo-ui.com/components/order-history-page): the playground lets you switch the starting status and order, change the tone, radius and list width, and try the loading state. The block, the tracker and the split view are part of Wingo UI Pro, which costs $8 a month, $80 a year or $150 for lifetime access as of November 2026 ([pricing](https://wingo-ui.com/pricing)). The 14-day refund applies only while no Pro item has been downloaded, so try everything in the playground first.

## Components in this post

- [Order History Page](https://wingo-ui.com/components/order-history-page) (Pro): A React order history page for an ecommerce account: orders grouped by month with search and filters, and a full order detail beside them. Install: `npx wingo-ui@latest add order-history-page`
- [Order tracker](https://wingo-ui.com/components/order-tracker) (Pro): A React order tracker: status, arrival window, milestones, a courier route, the tracking number, shipment history and the order total. Install: `npx wingo-ui@latest add order-tracker`
- [Checkout Page](https://wingo-ui.com/components/checkout-page) (Pro): A React checkout page from the cart to Order placed: an editable cart, four checkout steps and the order confirmation. Install: `npx wingo-ui@latest add checkout-page`
- [Split View](https://wingo-ui.com/components/split-view) (Pro): A React split view: a list and its detail side by side on wide screens, a push and pop stack with swipe back on phones. Install: `npx wingo-ui@latest add split-view`
- [Description List](https://wingo-ui.com/components/description-list) (Pro): A React description list for detail pages: key-value grids for clients, invoices and profiles, with copy buttons, skeletons and iOS rows. Install: `npx wingo-ui@latest add description-list`

## FAQ

### What should an order history page show for each order?

The order number, the date, a status badge, the item count, the total and small photos of the items, plus the actions that fit the status: Track while it is on the way, Buy again once it ended and Invoice unless it was cancelled. People should recognize an order without opening it.

### Should order details open on a separate page or beside the list?

Beside the list on wide screens, so people can compare orders without losing their place, and as its own full screen on phones, with a back button and swipe back. A modal works poorly here because the detail is long and people scroll it.

### How do I show a return window that has closed?

Keep a line in the order detail that says when the window closed, instead of silently removing the return button. Baymard's 2026 accounts research found that simply removing the Start Return button after the window closed caused needless uncertainty and wasted participants' time.

### Does the order history block take prices in cents or dollars?

Dollars. The Wingo UI block takes every amount in major units, so divide the cent amounts that Stripe and most databases store by 100 when you map your rows.

### Does search on a my orders page need a server endpoint?

Not when you can load a customer's orders in one response: the Wingo UI block filters on the client by order number, product name, variant and SKU. Accounts with hundreds of orders, such as B2B buyers, need server-side search and paging instead.

---

Source: https://wingo-ui.com/blog/order-history-page-design
