Orders page
An order index inside the app shell, with key figures, status filters, search, an orders table and pagination; each order opens in a Drawer.
From Figma 08 Overview Dashboard, Pattern 3 · Orders + drawer (7275:15971). Uses
PageHeader, Button,
KpiTile, Card,
TabNav, SearchField,
Table, Avatar,
Badge, NumberValue,
Drawer, Separator,
Text and Pagination.
The example is the page's content only: in the app it goes inside
AppShell, which owns the sidebar, the top bars and <main>.
// app/(store)/layout.tsx: the shell wraps every store page; the page renders its content
import { AppShell } from '@nizivo/react/app-shell';
<AppShell sidebar={sidebar} topBar={topBar} mobileTopBar={mobileTopBar} tabBar={tabBar}>
<OrdersPageExample />
</AppShell>;Example
Orders
Figma 08's Pattern 3, Orders: the content of the app shell's <main>. The page header, four key figures, then the orders card: status filters as links, a search form, the orders table and pagination. Each row's View opens that order in a Drawer from the end edge, over a scrim; the drawer is the only client part besides the search field.
Orders
| Order | Date | Customer | Payment | Fulfilment | Total | Details |
|---|---|---|---|---|---|---|
| #1042 | Maya Lindqvist | Paid | Unfulfilled | $68.00 | ||
| #1041 | Jonah Okafor | Paid | Unfulfilled | $72.00 | ||
| #1040 | Priya Shah | Pending | Unfulfilled | $90.00 | ||
| #1039 | Tomás Ribeiro | Paid | Fulfilled | $38.00 | ||
| #1038 | Aiko Tanaka | Paid | Fulfilled | $70.00 | ||
| #1037 | Sam Whitfield | Paid | Fulfilled | $36.00 |
import type { JSX } from 'react';
import { Download, Plus } from '@nizivo/icons';
import { Avatar } from '@nizivo/react/avatar';
import { Badge, type BadgeTone } from '@nizivo/react/badge';
import { Button } from '@nizivo/react/button';
import { Card } from '@nizivo/react/card';
import { Drawer } from '@nizivo/react/drawer';
import { KpiTile } from '@nizivo/react/kpi-tile';
import { NumberValue } from '@nizivo/react/number-value';
import { PageHeader } from '@nizivo/react/page-header';
import { Pagination } from '@nizivo/react/pagination';
import { SearchField } from '@nizivo/react/search-field';
import { Separator } from '@nizivo/react/separator';
import { TabNav } from '@nizivo/react/tab-nav';
import { Table } from '@nizivo/react/table';
import { Text } from '@nizivo/react/text';
/** Money in the store's currency: Intl formats it, never string concatenation. */
const USD = { style: 'currency', currency: 'USD' } as const;
/** An explicit locale and time zone, so the server and the browser print the same text. */
const WHEN = new Intl.DateTimeFormat('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hourCycle: 'h23',
timeZone: 'UTC',
});
type State = [label: string, tone: BadgeTone];
interface Order {
id: number;
placed: string;
customer: string;
shipTo: string;
lines: [title: string, quantity: number, total: number][];
payment: State;
fulfilment: State;
}
const PAID: State = ['Paid', 'success'];
const PENDING: State = ['Pending', 'warning'];
const UNFULFILLED: State = ['Unfulfilled', 'warning'];
const FULFILLED: State = ['Fulfilled', 'neutral'];
const ORDERS: Order[] = [
{
id: 1042,
placed: '2026-10-23T09:12:00Z',
customer: 'Maya Lindqvist',
shipTo: '14 Harbour Row, Bristol BS1 5TY',
lines: [
['Stoneware mug', 2, 56],
['Waffle tea towel', 1, 12],
],
payment: PAID,
fulfilment: UNFULFILLED,
},
{
id: 1041,
placed: '2026-10-23T08:47:00Z',
customer: 'Jonah Okafor',
shipTo: '2 Mill Lane, Leeds LS6 2AB',
lines: [['Speckled bowl', 3, 72]],
payment: PAID,
fulfilment: UNFULFILLED,
},
{
id: 1040,
placed: '2026-10-22T17:30:00Z',
customer: 'Priya Shah',
shipTo: '88 Elm Street, Brighton BN1 4EE',
lines: [
['Bud vase', 1, 32],
['Side plate', 4, 58],
],
payment: PENDING,
fulfilment: UNFULFILLED,
},
{
id: 1039,
placed: '2026-10-22T14:05:00Z',
customer: 'Tomás Ribeiro',
shipTo: '5 Quay Street, Cardiff CF10 1EA',
lines: [['Pouring jug', 1, 38]],
payment: PAID,
fulfilment: FULFILLED,
},
{
id: 1038,
placed: '2026-10-22T10:21:00Z',
customer: 'Aiko Tanaka',
shipTo: '41 Castle Road, York YO1 7HH',
lines: [
['Linen apron', 1, 42],
['Stoneware mug', 1, 28],
],
payment: PAID,
fulfilment: FULFILLED,
},
{
id: 1037,
placed: '2026-10-21T19:58:00Z',
customer: 'Sam Whitfield',
shipTo: '9 Garden Walk, Bath BA1 2QS',
lines: [['Coaster set', 2, 36]],
payment: PAID,
fulfilment: FULFILLED,
},
];
const TABS = [
['all', 'All'],
['unfulfilled', 'Unfulfilled'],
['unpaid', 'Unpaid'],
['open', 'Open'],
['archived', 'Archived'],
] as const;
function total(order: Order): number {
return order.lines.reduce((sum, [, , amount]) => sum + amount, 0);
}
/** One order's details, from the end edge: its lines, its total and where it ships. */
function OrderDrawer({ order }: { order: Order }): JSX.Element {
return (
<Drawer.Root>
<Drawer.Trigger
render={
<Button variant="tertiary" size="sm" aria-label={`View order ${String(order.id)}`}>
View
</Button>
}
/>
<Drawer.Popup side="end">
<Drawer.Header>
<Drawer.Title level={2}>Order #{order.id}</Drawer.Title>
<Drawer.Description>
<Badge tone={order.payment[1]}>{order.payment[0]}</Badge>{' '}
<Badge tone={order.fulfilment[1]}>{order.fulfilment[0]}</Badge>
</Drawer.Description>
</Drawer.Header>
<Drawer.Close />
<Drawer.Body>
<ul className="nzp-orders-page-lines">
{order.lines.map(([title, quantity, amount]) => (
<li key={title} className="nzp-orders-page-line">
<span className="nzp-orders-page-thumb" aria-hidden="true" />
<Text render={<span />} variant="label-m-strong">
{title}
</Text>
<Text render={<span />} variant="mono-m" tone="secondary">
×{quantity}
</Text>
<Text render={<span />} variant="numeric-m">
<NumberValue value={amount} format={USD} />
</Text>
</li>
))}
</ul>
<Separator />
<dl className="nzp-orders-page-facts">
<div>
<Text render={<dt />} tone="secondary">
Total
</Text>
<Text render={<dd />} variant="numeric-m">
<NumberValue value={total(order)} format={USD} />
</Text>
</div>
<div>
<Text render={<dt />} tone="secondary">
Placed
</Text>
<dd>
<time dateTime={order.placed}>{WHEN.format(new Date(order.placed))}</time>
</dd>
</div>
<div>
<Text render={<dt />} tone="secondary">
Ship to
</Text>
<dd>
{order.customer}, {order.shipTo}
</dd>
</div>
</dl>
</Drawer.Body>
<Drawer.Footer>
<Button>Print label</Button>
<Button variant="primary">Fulfil order</Button>
</Drawer.Footer>
</Drawer.Popup>
</Drawer.Root>
);
}
export function OrdersPageExample(): JSX.Element {
return (
<div className="nzp-orders-page">
<PageHeader
level={2}
title="Orders"
actions={
<>
<Button leadingIcon={<Download />}>Export</Button>
<Button variant="primary" leadingIcon={<Plus />}>
Create order
</Button>
</>
}
/>
<section aria-label="Key figures" className="nzp-orders-page-kpis">
<KpiTile label="Orders today" value={38} delta={0.118} comparison="vs yesterday" />
<KpiTile label="Items ordered" value={74} delta={0.064} comparison="vs yesterday" />
<KpiTile
label="Average order value"
value={64.2}
format={USD}
delta={0.031}
comparison="vs last week"
/>
<KpiTile
label="Fulfilled on time"
value={0.964}
format={{ style: 'percent', maximumFractionDigits: 1 }}
delta={0.012}
comparison="vs last week"
/>
</section>
<Card.Root className="nzp-orders-page-card">
<div className="nzp-orders-page-toolbar">
<TabNav.Root aria-label="Order status">
{TABS.map(([value, label]) => (
<TabNav.Link key={value} href={`?status=${value}`} current={value === 'all'}>
{label}
</TabNav.Link>
))}
</TabNav.Root>
<form method="get" className="nzp-orders-page-search">
<SearchField name="q" placeholder="Search orders" labels={{ label: 'Search orders' }} />
</form>
</div>
<Table.Container className="nzp-orders-page-table">
<Table.Root>
<Table.Caption visuallyHidden>Orders</Table.Caption>
<Table.Header>
<Table.Row>
<Table.Head>Order</Table.Head>
<Table.Head>Date</Table.Head>
<Table.Head>Customer</Table.Head>
<Table.Head>Payment</Table.Head>
<Table.Head>Fulfilment</Table.Head>
<Table.Head numeric>Total</Table.Head>
<Table.Head>Details</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{ORDERS.map((order) => (
<Table.Row key={order.id}>
<Table.Head scope="row">
<Text render={<span />} variant="mono-m">
#{order.id}
</Text>
</Table.Head>
<Table.Cell>
<time dateTime={order.placed}>{WHEN.format(new Date(order.placed))}</time>
</Table.Cell>
<Table.Cell>
<span className="nzp-orders-page-customer">
<Avatar name={order.customer} size="sm" decorative />
{order.customer}
</span>
</Table.Cell>
<Table.Cell>
<Badge tone={order.payment[1]}>{order.payment[0]}</Badge>
</Table.Cell>
<Table.Cell>
<Badge tone={order.fulfilment[1]}>{order.fulfilment[0]}</Badge>
</Table.Cell>
<Table.Cell numeric>
<NumberValue value={total(order)} format={USD} />
</Table.Cell>
<Table.Cell>
<OrderDrawer order={order} />
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
</Table.Container>
<Pagination
className="nzp-orders-page-pagination"
page={1}
totalPages={9}
size="sm"
siblingCount={0}
getHref={(page) => `?page=${String(page)}`}
summary="Showing 1–6 of 52"
/>
</Card.Root>
</div>
);
}
Stylesheet
@layer nz.tokens, nz.themes, nz.reset, nz.base, nz.components, nz.patterns, nz.utilities;
/* Figma 08 Pattern 3 (7275:15971): the header, the KPI row (16 apart, as Figma's KPI rows are), then
the orders card with its toolbar, the table full-bleed and the footer. The drawer's contents are
the order's lines and a short list of facts. The KPI row follows the page's own width. */
@layer nz.patterns {
.nzp-orders-page {
display: flex;
flex-direction: column;
gap: var(--nz-space-6);
min-inline-size: 0;
container-type: inline-size;
}
.nzp-orders-page-kpis {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: var(--nz-space-4);
}
@container (width >= 30rem) {
.nzp-orders-page-kpis {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@container (width >= 56rem) {
.nzp-orders-page-kpis {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
/* the parts pad themselves, so the table can run to the card's edges */
.nzp-orders-page-card {
gap: 0;
padding: 0;
overflow: hidden;
}
.nzp-orders-page-toolbar {
display: flex;
flex-direction: column;
gap: var(--nz-space-4);
padding: var(--nz-space-4) var(--nz-space-6);
}
.nzp-orders-page-search {
max-inline-size: 30rem;
}
/* the card draws the edge: the table loses its own side borders and radius */
.nzp-orders-page-table {
border-inline-width: 0;
border-radius: 0;
}
.nzp-orders-page-customer {
display: inline-flex;
align-items: center;
gap: var(--nz-space-2);
}
.nzp-orders-page-pagination {
padding: var(--nz-space-4) var(--nz-space-6);
}
.nzp-orders-page-lines {
display: flex;
flex-direction: column;
gap: var(--nz-space-3);
margin: 0;
padding: 0;
list-style: none;
}
.nzp-orders-page-line {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto auto;
align-items: center;
gap: var(--nz-space-3);
}
.nzp-orders-page-thumb {
inline-size: 2.5rem;
block-size: 2.5rem;
border-radius: var(--nz-radius-sm);
background: var(--nz-bg-muted);
}
.nzp-orders-page-facts {
display: flex;
flex-direction: column;
gap: var(--nz-space-3);
margin: 0;
}
.nzp-orders-page-facts > div {
display: grid;
grid-template-columns: 5rem minmax(0, 1fr);
gap: var(--nz-space-3);
}
.nzp-orders-page-facts dd {
margin: 0;
}
}
Notes
- Heading levels. This site's page already has its
<h1>, so the example passeslevel={2}to PageHeader. In your app, droplevel(the title becomes the page's<h1>). - The drawer. Each row's View is the trigger of its own
Drawer, which opens from the end edge over a scrim, moves focus into it and returns focus to the button when it closes. Without a client component of your own there is no shared open state, so each row carries its drawer; in an app that renders the order from the URL (?order=1042), one controlled Drawer is enough. - Dates.
Intl.DateTimeFormatwith an explicit locale andtimeZone, so the server and the browser print the same text. - Not in Figma. Figma's second header action is unread; the example uses "Create order". Figma has no mobile design for this page, so the table scrolls sideways inside its card on a narrow screen.
Product detail page
A product's edit form inside the app shell, with a breadcrumb, media, details, variants and pricing beside its status, sales and inventory.
Customers page
A customer index inside the app shell, with key figures, segment links and a customers table beside the top cities and a win-back card. It ships no JavaScript.