Analytics page
A report inside the app shell, with a range control, key figures with sparklines, a sales trend, goals, sales by channel, orders per day and the top products.
From Figma 08 Overview Dashboard, Pattern 6 · Analytics (7275:16296). Uses
PageHeader, SegmentedControl,
Button, KpiTile,
TrendCard, Card,
ProgressRing, Text and
RankedList from @nizivo/react, and
Sparkline, AreaChart,
Legend, DonutChart and
BarChart from @nizivo/charts.
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}>
<AnalyticsPageExample />
</AppShell>;Example
Analytics
Figma 08's Pattern 6, Analytics: the content of the app shell's <main>. The page header with the range (a SegmentedControl) and Export, four key figures with sparklines, then the report: total sales against the previous period, goals as progress rings, the top products, sales by channel and orders per day. Each chart sits in a Card whose title names it; the chart keeps that name for its figure and its data table, with its own caption hidden.
Analytics
| Date | This period | Previous |
|---|---|---|
| Thu, Oct 1 | $1,300 | $1,430 |
| Fri, Oct 2 | $1,476 | $1,429 |
| Sat, Oct 3 | $1,629 | $1,409 |
| Sun, Oct 4 | $1,740 | $1,374 |
| Mon, Oct 5 | $1,640 | $1,328 |
| Tue, Oct 6 | $1,649 | $1,278 |
| Wed, Oct 7 | $1,617 | $1,229 |
| Thu, Oct 8 | $1,564 | $1,189 |
| Fri, Oct 9 | $1,353 | $1,162 |
| Sat, Oct 10 | $1,327 | $1,153 |
| Sun, Oct 11 | $1,344 | $1,163 |
| Mon, Oct 12 | $1,413 | $1,193 |
| Tue, Oct 13 | $1,372 | $1,240 |
| Wed, Oct 14 | $1,531 | $1,300 |
| Thu, Oct 15 | $1,709 | $1,368 |
| Fri, Oct 16 | $1,883 | $1,436 |
| Sat, Oct 17 | $1,869 | $1,499 |
| Sun, Oct 18 | $1,972 | $1,550 |
| Mon, Oct 19 | $2,022 | $1,585 |
| Tue, Oct 20 | $2,022 | $1,601 |
| Wed, Oct 21 | $1,825 | $1,597 |
| Thu, Oct 22 | $1,771 | $1,575 |
| Fri, Oct 23 | $1,723 | $1,538 |
| Sat, Oct 24 | $1,703 | $1,491 |
| Sun, Oct 25 | $1,569 | $1,440 |
| Mon, Oct 26 | $1,647 | $1,392 |
| Tue, Oct 27 | $1,775 | $1,353 |
| Wed, Oct 28 | $1,939 | $1,329 |
| Thu, Oct 29 | $1,958 | $1,323 |
| Fri, Oct 30 | $2,128 | $1,336 |
- This period
- Previous
Goals
- Monthly sales
- Orders
- New reviews
- Subscribers
Top products
View all- Stoneware mug$8,960
- Speckled bowl$6,240
- Linen apron$4,410
- Pouring jug$3,180
- Bud vase$2,240
Sales by channel
- Web store$26,41055%
- Instagram$12,88027%
- Markets$6,12013%
- Wholesale$2,8806%
| Channel | Sales | Share |
|---|---|---|
| Web store | $26,410 | 55% |
| $12,880 | 27% | |
| Markets | $6,120 | 13% |
| Wholesale | $2,880 | 6% |
Orders per day
import type { JSX } from 'react';
import { Download } from '@nizivo/icons';
import { AreaChart } from '@nizivo/charts/area-chart';
import { BarChart } from '@nizivo/charts/bar-chart';
import { DonutChart } from '@nizivo/charts/donut-chart';
import { Legend } from '@nizivo/charts/legend';
import { Sparkline } from '@nizivo/charts/sparkline';
import { Button } from '@nizivo/react/button';
import { Card } from '@nizivo/react/card';
import { KpiTile } from '@nizivo/react/kpi-tile';
import { PageHeader } from '@nizivo/react/page-header';
import { ProgressRing } from '@nizivo/react/progress-ring';
import { RankedList } from '@nizivo/react/ranked-list';
import { SegmentedControl } from '@nizivo/react/segmented-control';
import { Text } from '@nizivo/react/text';
import { TrendCard } from '@nizivo/react/trend-card';
const USD = { style: 'currency', currency: 'USD', maximumFractionDigits: 0 } as const;
const PERCENT = { style: 'percent', maximumFractionDigits: 1 } as const;
const RANGES = [
['today', 'Today'],
['7d', '7D'],
['30d', '30D'],
['90d', '90D'],
['12m', '12M'],
] as const;
interface Kpi {
label: string;
value: number;
format?: Intl.NumberFormatOptions;
delta: number;
trend: number[];
}
const KPIS: Kpi[] = [
{ label: 'Sessions', value: 18240, delta: 0.094, trend: [40, 42, 41, 45, 44, 47, 49, 52, 55] },
{
label: 'Conversion',
value: 0.032,
format: PERCENT,
delta: -0.031,
trend: [27, 25, 26, 22, 23, 20, 21, 18, 17],
},
{
label: 'Total sales',
value: 48290,
format: USD,
delta: 0.124,
trend: [12, 14, 13, 17, 16, 19, 18, 22, 27],
},
{
label: 'Bounce rate',
value: 0.412,
format: PERCENT,
delta: 0.018,
trend: [30, 31, 30, 32, 33, 32, 34, 33, 35],
},
];
interface Day {
date: string;
sales: number;
previous: number;
orders: number;
}
/** Thirty days of fictional sales, deterministic so the server and the browser agree. */
const DAYS: Day[] = Array.from({ length: 30 }, (_, i) => ({
date: `2026-10-${String(i + 1).padStart(2, '0')}`,
sales: Math.round(1300 + i * 22 + Math.sin(i / 2.2) * 260 + (i % 4) * 40),
previous: Math.round(1250 + i * 9 + Math.cos(i / 3) * 180),
orders: Math.round(34 + i * 0.6 + Math.sin(i / 2.2) * 7 + (i % 4)),
}));
const CHANNELS = [
{ name: 'Web store', sales: 26410 },
{ name: 'Instagram', sales: 12880 },
{ name: 'Markets', sales: 6120 },
{ name: 'Wholesale', sales: 2880 },
];
const GOALS = [
['sales', 'Monthly sales', 75],
['orders', 'Orders', 50],
['reviews', 'New reviews', 100],
['subscribers', 'Subscribers', 25],
] as const;
const PRODUCTS = [
['Stoneware mug', 8960],
['Speckled bowl', 6240],
['Linen apron', 4410],
['Pouring jug', 3180],
['Bud vase', 2240],
] as const;
export function AnalyticsPageExample(): JSX.Element {
const total = DAYS.reduce((sum, day) => sum + day.sales, 0);
return (
<div className="nzp-analytics-page">
<PageHeader
level={2}
title="Analytics"
actions={
<>
<SegmentedControl.Root aria-label="Range" name="range" defaultValue="30d">
{RANGES.map(([value, label]) => (
<SegmentedControl.Item key={value} value={value}>
{label}
</SegmentedControl.Item>
))}
</SegmentedControl.Root>
<Button leadingIcon={<Download />}>Export</Button>
</>
}
/>
<section aria-label="Key figures" className="nzp-analytics-page-kpis">
{KPIS.map((kpi) => (
<KpiTile
key={kpi.label}
label={kpi.label}
value={kpi.value}
format={kpi.format}
delta={kpi.delta}
comparison="vs last period"
chart={<Sparkline data={kpi.trend} />}
/>
))}
</section>
<div className="nzp-analytics-page-report">
<TrendCard
className="nzp-analytics-page-wide"
label="Total sales"
value={total}
format={USD}
delta={0.124}
comparison="vs previous 30 days"
chart={
<AreaChart
title="Sales"
summary="Daily sales, Oct 1–30, vs the previous 30 days"
titleVisibility="hidden"
data={DAYS}
x="date"
y="sales"
compare="previous"
format={USD}
legend={false}
/>
}
legend={
<Legend
items={[
{ label: 'This period', series: 'current' },
{ label: 'Previous', series: 'previous' },
]}
/>
}
/>
<Card.Root render={<section aria-labelledby="nzp-analytics-goals" />}>
<Card.Header>
<Card.Title id="nzp-analytics-goals">Goals</Card.Title>
</Card.Header>
<ul className="nzp-analytics-page-goals">
{GOALS.map(([id, label, value]) => (
<li key={id} className="nzp-analytics-page-goal">
<ProgressRing
meter
value={value}
tone={value === 100 ? 'success' : 'brand'}
aria-labelledby={`nzp-analytics-goal-${id}`}
/>
<Text render={<span />} id={`nzp-analytics-goal-${id}`} variant="label-m">
{label}
</Text>
</li>
))}
</ul>
</Card.Root>
<RankedList.Root
title="Top products"
format={USD}
action={<RankedList.Action href="/analytics/products">View all</RankedList.Action>}
>
{PRODUCTS.map(([label, value]) => (
<RankedList.Item key={label} label={label} value={value} />
))}
</RankedList.Root>
<Card.Root
className="nzp-analytics-page-wide"
render={<section aria-labelledby="nzp-analytics-channels" />}
>
<Card.Header>
<Card.Title id="nzp-analytics-channels">Sales by channel</Card.Title>
</Card.Header>
<DonutChart
title="Sales by channel"
summary="Share of total sales, October"
titleVisibility="hidden"
data={CHANNELS}
category={(d) => d.name}
value={(d) => d.sales}
format={USD}
labels={{ category: 'Channel', value: 'Sales', total: 'Total sales' }}
/>
</Card.Root>
<Card.Root
className="nzp-analytics-page-wide"
render={<section aria-labelledby="nzp-analytics-orders" />}
>
<Card.Header>
<Card.Title id="nzp-analytics-orders">Orders per day</Card.Title>
</Card.Header>
<BarChart
title="Orders per day"
summary="Orders per day, Oct 17–30"
titleVisibility="hidden"
data={DAYS.slice(16)}
x={(d) => d.date}
y={(d) => d.orders}
dateFormat={{ day: 'numeric' }}
labels={{ category: 'Day', value: 'Orders' }}
/>
</Card.Root>
</div>
</div>
);
}
Stylesheet
@layer nz.tokens, nz.themes, nz.reset, nz.base, nz.components, nz.patterns, nz.utilities;
/* Figma 08 Pattern 6 (7275:16296): the header, the KPI row (16 apart), then the report's cards, 16
apart as Figma's rows are. Figma's row widths (798 / 314, 524 / 588, 690 / 422) match no column
span, so the report is two equal columns once the page is 36rem wide, the three charts across both
(the donut's legend and the bars need the width); one column below that. The grids follow the page's own width. */
@layer nz.patterns {
.nzp-analytics-page {
display: flex;
flex-direction: column;
gap: var(--nz-space-6);
min-inline-size: 0;
container-type: inline-size;
}
.nzp-analytics-page-kpis {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: var(--nz-space-4);
}
.nzp-analytics-page-report {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: var(--nz-space-4);
}
@container (width >= 30rem) {
.nzp-analytics-page-kpis {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@container (width >= 36rem) {
.nzp-analytics-page-report {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.nzp-analytics-page-wide {
grid-column: 1 / -1;
}
}
@container (width >= 56rem) {
.nzp-analytics-page-kpis {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
.nzp-analytics-page-goals {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--nz-space-5) var(--nz-space-4);
margin: 0;
padding: 0;
list-style: none;
}
.nzp-analytics-page-goal {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--nz-space-2);
text-align: center;
}
}
Your app also loads @nizivo/charts/styles.css once, as the
charts setup shows.
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>). - Charts in cards. Each chart's Card title names it on screen; the chart takes the same
titlewithtitleVisibility="hidden", so its figure and its data table keep the name. The area, bar and donut charts load their small interaction island; the sparklines ship nothing. - The range. The SegmentedControl submits
rangewith a form, or drives the data from the client; the example leaves that wiring to your app. - Layout. Figma's row widths (798 / 314, 524 / 588, 690 / 422) match no column span, so the report is two equal columns from 36rem, the three charts across both.
- Not built. Figma's Funnel chart ("Checkout funnel") and Heatmap ("Orders by hour") have no
component in
@nizivo/charts, so the example leaves both out and shows the sales trend in their place.
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.
Plan and billing page
A settings page inside the app shell, with a section nav beside the current plan, its usage on Meters, the payment method, the invoices table and billing emails.