Skip to content
Nizivo UI

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

Customers
1,280
+4.2%vs last month
Returning rate
28.4%
+2.1%vs last month
Average lifetime value
$142
-1.3%vs last month
Customers
CustomerCityOrdersSpent
Maya LindqvistVIPmaya@example.comBristol14$1,284.50
Jonah OkaforVIPjonah@example.comLeeds9$642.00
Priya Shahpriya@example.comBrighton6$418.20
Tomás Ribeirotomas@example.comCardiff4$236.00
Aiko Tanakaaiko@example.comYork3$154.50
Sam Whitfieldsam@example.comBath1$36.00

Top cities

View all
  1. Bristol186
  2. Leeds142
  3. Brighton118
  4. Cardiff96
  5. 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

customers-page.css
@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 passes level={2} to PageHeader. In your app, drop level (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 src in your app.
  • Not built. Figma's Insight Card, with its 3D render, has no component: the win-back prompt is an accent Card with no media.

On this page