Skip to content
Nizivo UI

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

Orders today
38
+11.8%vs yesterday
Items ordered
74
+6.4%vs yesterday
Average order value
$64.20
+3.1%vs last week
Fulfilled on time
96.4%
+1.2%vs last week
Orders
OrderDateCustomerPaymentFulfilmentTotalDetails
#1042Maya LindqvistPaidUnfulfilled$68.00
#1041Jonah OkaforPaidUnfulfilled$72.00
#1040Priya ShahPendingUnfulfilled$90.00
#1039Tomás RibeiroPaidFulfilled$38.00
#1038Aiko TanakaPaidFulfilled$70.00
#1037Sam WhitfieldPaidFulfilled$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

orders-page.css
@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 passes level={2} to PageHeader. In your app, drop level (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.DateTimeFormat with an explicit locale and timeZone, 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.

On this page