ComponentsData display

Metric tile

One number for the period, how it moved against the previous period, its target, and a small chart of the period. The Dashboard's headline grid is six of them.

Playground

Change the controls to try each prop. The code under the preview updates as you go and lists only the props you changed; copy it into your page.

CVs sent

48Up 12%

Target 45

States
import { MetricTile } from '@/ui/metric-tile';<MetricTile label="CVs sent" value="48" change="12%" direction="up" target="Target 45" />

Default

The label (a link to the records behind the number), the figure in tabular digits, the change against the previous period, the target and the period's chart. A fall is neutral, never red, and Behind target is a neutral pill.

CVs sent

48Up 12%

Target 45

1 Oct15 Oct

Placements

3Down 25%

Target 5Behind target

1 Oct15 Oct
import { LineChart } from '@/ui/chart';
import { MetricTile } from '@/ui/metric-tile';

const days = ['1 Oct', '3 Oct', '5 Oct', '7 Oct', '9 Oct', '11 Oct', '13 Oct', '15 Oct'];
const series = (values: number[]) => values.map((value, i) => ({ label: days[i]!, value, text: String(value) }));

export default function MetricTileDefault() {
  return (
    <div className="grid w-full grid-cols-1 gap-300 sm:grid-cols-2">
      <MetricTile
        label="CVs sent"
        href="#cvs-sent"
        source="CVs sent to clients in this period, from each Application's CV sent date"
        value="48"
        change="12%" direction="up"
        target="Target 45"
        chart={<LineChart size="compact" name="CVs sent" current={series([2, 4, 3, 6, 5, 8, 7, 11])} previous={series([1, 3, 4, 3, 5, 4, 6, 5])} />}
      />
      <MetricTile
        label="Placements"
        href="#placements"
        value="3"
        change="25%" direction="down"
        target="Target 5"
        behind
        chart={<LineChart size="compact" name="Placements" current={series([0, 0, 1, 1, 2, 2, 3, 3])} previous={series([0, 1, 1, 2, 2, 3, 3, 4])} />}
      />
    </div>
  );
}

Large and small figure

Large for the one number a card is about. Money goes in money: written in the reader's number format with its ISO code and no symbol, and never added across currencies, so a second currency goes in the note. Small when a card holds several figures side by side: pass them as Metric size="small" children.

Invoiced fee

148,500 GBPNo change 0%

Also 12,000 USD. The chart shows GBP.

Placed fee

96,000 GBP

Overdue now

18,200 GBP

2 invoices

import { Metric, MetricTile } from '@/ui/metric-tile';

// Money keeps its code and is never added across currencies: the lead currency is the figure, the rest go in the note.
export default function MetricTileSizes() {
  return (
    <div className="grid w-full grid-cols-1 items-start gap-300 sm:grid-cols-2">
      <MetricTile
        label="Invoiced fee"
        locale="en-GB"
        money={[
          { amount: 148500, currency: 'GBP' },
          { amount: 12000, currency: 'USD' },
        ]}
        change="0%"
        note="The chart shows GBP."
      />
      <MetricTile>
        <Metric size="small" label="Placed fee" locale="en-GB" money={[{ amount: 96000, currency: 'GBP' }]} />
        <Metric size="small" label="Overdue now" locale="en-GB" money={[{ amount: 18200, currency: 'GBP' }]} note="2 invoices" />
      </MetricTile>
    </div>
  );
}

States

Hover or focus the Offers label: ink and an underline, and the card itself doesn't move. Loading draws skeleton bars at the label and figure size; press Replay. Empty keeps zero as a figure and the chart gives way to one line.

Loading Interviews

Collected fee

£0

Nothing collected in this period

import { useState } from 'react';
import { Button } from '@/ui/button';
import { LineChart } from '@/ui/chart';
import { MetricTile } from '@/ui/metric-tile';

// Press Replay: skeleton bars at the label and figure size while the numbers load.
export default function MetricTileStates() {
  const [loading, setLoading] = useState(true);
  const replay = () => {
    setLoading(true);
    setTimeout(() => setLoading(false), 1500);
  };
  return (
    <div className="flex w-full flex-col items-start gap-200">
      <div className="grid w-full grid-cols-1 items-start gap-300 sm:grid-cols-3">
        <MetricTile label="Offers" href="#offers" value="6" change="2" direction="up" />
        <MetricTile label="Interviews" loading={loading} value="14" change="3" direction="up" />
        <MetricTile
          label="Collected fee"
          value="£0"
          chart={<LineChart size="compact" name="Collected fee" empty="Nothing collected in this period" current={[{ label: '1 Oct', value: 0, text: '£0' }, { label: '15 Oct', value: 0, text: '£0' }]} />}
        />
      </div>
      <Button size="sm" onClick={replay}>
        Replay
      </Button>
    </div>
  );
}

Edge cases

A large amount keeps every digit unless the tile is short. A figure with nothing behind it is an em dash, never NaN or null. A long or non-Latin label wraps; a large change stays as written.

Pipeline value

2.450.000.000 VND

Pipeline value, short

2,5 T VND

Conversion

No figure

No CVs sent in this period

Placements by Nguyễn Thị Minh Khai and Zoë O'Brien-Smith

1,234,567Up 99+

import { MetricTile } from '@/ui/metric-tile';

// A huge figure keeps every digit unless the tile says short; a missing figure is an em dash, never NaN; long and
// non-Latin labels wrap.
export default function MetricTileEdgeCases() {
  return (
    <div className="grid w-full grid-cols-1 items-start gap-300 sm:grid-cols-2 lg:grid-cols-4">
      <MetricTile label="Pipeline value" locale="vi-VN" money={[{ amount: 2450000000, currency: 'VND' }]} />
      <MetricTile label="Pipeline value, short" locale="vi-VN" short money={[{ amount: 2450000000, currency: 'VND' }]} />
      <MetricTile label="Conversion" value={Number.NaN} note="No CVs sent in this period" />
      <MetricTile label="Placements by Nguyễn Thị Minh Khai and Zoë O'Brien-Smith" value="1,234,567" change="99+" direction="up" />
    </div>
  );
}

On this page