Skip to content
Nizivo UI

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

Sessions
18,240
+9.4%vs last period
Conversion
3.2%
-3.1%vs last period
Total sales
$48,290
+12.4%vs last period
Bounce rate
41.2%
+1.8%vs last period
Total sales
$50,470
+12.4%vs previous 30 days
SalesDaily sales, Oct 1–30, vs the previous 30 days
Arrow keys move between points; Home and End go to the first and last
DateThis periodPrevious
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
  1. Stoneware mug$8,960
  2. Speckled bowl$6,240
  3. Linen apron$4,410
  4. Pouring jug$3,180
  5. Bud vase$2,240

Sales by channel

Sales by channelShare of total sales, October
  • Web store$26,41055%
  • Instagram$12,88027%
  • Markets$6,12013%
  • Wholesale$2,8806%
$48.3KTotal sales
Arrow keys move between slices; Home and End go to the first and last
ChannelSalesShare
Web store$26,41055%
Instagram$12,88027%
Markets$6,12013%
Wholesale$2,8806%

Orders per day

Orders per dayOrders per day, Oct 17–30
Arrow keys move between bars; Home and End go to the first and last
DayOrders
Sat, Oct 1749
Sun, Oct 1852
Mon, Oct 1953
Tue, Oct 2053
Wed, Oct 2148
Thu, Oct 2247
Fri, Oct 2345
Sat, Oct 2445
Sun, Oct 2541
Mon, Oct 2643
Tue, Oct 2747
Wed, Oct 2851
Thu, Oct 2952
Fri, Oct 3056
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

analytics-page.css
@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 passes level={2} to PageHeader. In your app, drop level (the title becomes the page's <h1>).
  • Charts in cards. Each chart's Card title names it on screen; the chart takes the same title with titleVisibility="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 range with 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.

On this page