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.
From Figma 08 Overview Dashboard, Pattern 4 · Customers (7275:16097). Uses
PageHeader, Button,
KpiTile, TabNav,
Split, Table,
Avatar, Badge,
Text, NumberValue,
RankedList and Card.
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}>
<CustomersPageExample />
</AppShell>;Example
Customers
Figma 08's Pattern 4, Customers: the content of the app shell's <main>. The page header, three key figures and the segment links, then two columns: the customers table, each name with its Avatar (initials here; a photo through src in the app), and an aside with the top cities and a win-back prompt. A server composition: it ships no JavaScript.
Customers
| Customer | City | Orders | Spent |
|---|---|---|---|
| Maya LindqvistVIPmaya@example.com | Bristol | 14 | $1,284.50 |
| Jonah OkaforVIPjonah@example.com | Leeds | 9 | $642.00 |
| Priya Shahpriya@example.com | Brighton | 6 | $418.20 |
| Tomás Ribeirotomas@example.com | Cardiff | 4 | $236.00 |
| Aiko Tanakaaiko@example.com | York | 3 | $154.50 |
| Sam Whitfieldsam@example.com | Bath | 1 | $36.00 |
Top cities
View all- Bristol186
- Leeds142
- Brighton118
- Cardiff96
- York71
Win back 214 lapsed buyers
They last ordered more than 90 days ago. A small thank-you code brings many back.
import type { JSX } from 'react';
import { Download, UserPlus } from '@nizivo/icons';
import { Avatar } from '@nizivo/react/avatar';
import { Badge } from '@nizivo/react/badge';
import { Button } from '@nizivo/react/button';
import { Card } from '@nizivo/react/card';
import { KpiTile } from '@nizivo/react/kpi-tile';
import { NumberValue } from '@nizivo/react/number-value';
import { PageHeader } from '@nizivo/react/page-header';
import { RankedList } from '@nizivo/react/ranked-list';
import { Split } from '@nizivo/react/split';
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;
interface Customer {
id: string;
name: string;
email: string;
city: string;
orders: number;
spent: number;
vip: boolean;
}
const CUSTOMERS: Customer[] = [
{
id: 'c-201',
name: 'Maya Lindqvist',
email: 'maya@example.com',
city: 'Bristol',
orders: 14,
spent: 1284.5,
vip: true,
},
{
id: 'c-187',
name: 'Jonah Okafor',
email: 'jonah@example.com',
city: 'Leeds',
orders: 9,
spent: 642,
vip: true,
},
{
id: 'c-176',
name: 'Priya Shah',
email: 'priya@example.com',
city: 'Brighton',
orders: 6,
spent: 418.2,
vip: false,
},
{
id: 'c-163',
name: 'Tomás Ribeiro',
email: 'tomas@example.com',
city: 'Cardiff',
orders: 4,
spent: 236,
vip: false,
},
{
id: 'c-158',
name: 'Aiko Tanaka',
email: 'aiko@example.com',
city: 'York',
orders: 3,
spent: 154.5,
vip: false,
},
{
id: 'c-142',
name: 'Sam Whitfield',
email: 'sam@example.com',
city: 'Bath',
orders: 1,
spent: 36,
vip: false,
},
];
const SEGMENTS = [
['all', 'All', '1,280'],
['vip', 'VIP', '24'],
['subscribers', 'Subscribers', '311'],
['at-risk', 'At risk', '214'],
] as const;
const CITIES = [
['Bristol', 186],
['Leeds', 142],
['Brighton', 118],
['Cardiff', 96],
['York', 71],
] as const;
export function CustomersPageExample(): JSX.Element {
return (
<div className="nzp-customers-page">
<PageHeader
level={2}
title="Customers"
actions={
<>
<Button leadingIcon={<Download />}>Export</Button>
<Button variant="primary" leadingIcon={<UserPlus />}>
Add customer
</Button>
</>
}
/>
<section aria-label="Key figures" className="nzp-customers-page-kpis">
<KpiTile label="Customers" value={1280} delta={0.042} comparison="vs last month" />
<KpiTile
tone="brand"
label="Returning rate"
value={0.284}
format={{ style: 'percent', maximumFractionDigits: 1 }}
delta={0.021}
comparison="vs last month"
/>
<KpiTile
label="Average lifetime value"
value={142}
format={{ ...USD, maximumFractionDigits: 0 }}
delta={-0.013}
comparison="vs last month"
/>
</section>
<TabNav.Root aria-label="Customer segments">
{SEGMENTS.map(([value, label, count]) => (
<TabNav.Link
key={value}
href={`?segment=${value}`}
count={count}
current={value === 'all'}
>
{label}
</TabNav.Link>
))}
</TabNav.Root>
<Split.Root>
<Split.Main>
<Table.Container>
<Table.Root>
<Table.Caption visuallyHidden>Customers</Table.Caption>
<Table.Header>
<Table.Row>
<Table.Head>Customer</Table.Head>
<Table.Head>City</Table.Head>
<Table.Head numeric>Orders</Table.Head>
<Table.Head numeric>Spent</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{CUSTOMERS.map((customer) => (
<Table.Row key={customer.id} interactive>
<Table.Cell>
<span className="nzp-customers-page-person">
<Avatar name={customer.name} decorative />
<span className="nzp-customers-page-name">
<span className="nzp-customers-page-title">
<Table.RowLink href={`/customers/${customer.id}`}>
{customer.name}
</Table.RowLink>
{customer.vip && <Badge tone="brand">VIP</Badge>}
</span>
<Text render={<span />} tone="secondary">
{customer.email}
</Text>
</span>
</span>
</Table.Cell>
<Table.Cell>{customer.city}</Table.Cell>
<Table.Cell numeric>{customer.orders}</Table.Cell>
<Table.Cell numeric>
<NumberValue value={customer.spent} format={USD} />
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
</Table.Container>
</Split.Main>
<Split.Aside className="nzp-customers-page-aside">
<RankedList.Root
title="Top cities"
action={<RankedList.Action href="/customers/cities">View all</RankedList.Action>}
>
{CITIES.map(([city, count]) => (
<RankedList.Item key={city} label={city} value={count} />
))}
</RankedList.Root>
<Card.Root tone="accent" render={<section aria-labelledby="nzp-customers-win-back" />}>
<Card.Header>
<Card.Title id="nzp-customers-win-back">Win back 214 lapsed buyers</Card.Title>
<Card.Description>
They last ordered more than 90 days ago. A small thank-you code brings many back.
</Card.Description>
</Card.Header>
<Card.Footer>
<Button variant="primary">Create a campaign</Button>
</Card.Footer>
</Card.Root>
</Split.Aside>
</Split.Root>
</div>
);
}
Stylesheet
@layer nz.tokens, nz.themes, nz.reset, nz.base, nz.components, nz.patterns, nz.utilities;
/* Figma 08 Pattern 4 (7275:16097): the header, the KPI row (16 apart), the segment links, then
Split's two columns: the customers table and an aside of the top cities and a win-back card. The
KPI row follows the page's own width. */
@layer nz.patterns {
.nzp-customers-page {
display: flex;
flex-direction: column;
gap: var(--nz-space-6);
min-inline-size: 0;
container-type: inline-size;
}
.nzp-customers-page-kpis {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: var(--nz-space-4);
}
@container (width >= 36rem) {
.nzp-customers-page-kpis {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.nzp-customers-page-aside {
display: flex;
flex-direction: column;
gap: var(--nz-space-6);
}
.nzp-customers-page-person {
display: flex;
align-items: center;
gap: var(--nz-space-3);
}
.nzp-customers-page-name {
display: flex;
flex-direction: column;
gap: var(--nz-space-0-5);
}
.nzp-customers-page-title {
display: flex;
align-items: center;
gap: var(--nz-space-2);
}
}
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>). - A table, not cards. Figma lays the customers out as Customer Cards, two to a row; that card
has no component, and a table reads and sorts better at this size, so the example uses one. Each
name is the row's link (
Table.RowLink), so the whole row is the hit area. - Avatars. The avatar is decorative beside the printed name; it shows initials here, and a photo
through
srcin your app. - Not built. Figma's Insight Card, with its 3D render, has no component: the win-back prompt is
an accent
Cardwith no media.
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.
Discounts page
A discount index inside the app shell, with a notice, key figures, status links and a card per discount showing its status, dates and usage on a Meter. It ships no JavaScript.