# KPI Dashboard

> A React KPI dashboard: metric tiles with deltas, revenue against the last period, sources, funnel, top sellers, invoices and activity.

- Type: React block (Blocks)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.1
- Install: `npx wingo-ui@latest add kpi-dashboard`
- Docs: https://wingo-ui.com/components/kpi-dashboard

## Interaction

Pick the period in the header: presets (Today, Last 7 days, This month, Last month, This quarter, This year) or any range on the calendar, applied with Apply; on a phone it opens as a bottom sheet. The compare switch turns the dashed previous period in the revenue chart, the KPI deltas and the salespeople's changes on and off. Refresh spins its icon, shows a thin bar under the header and announces "Updated at 16:40" when it is done; a refreshShortcut binds it to a key. Export opens a menu with CSV and PDF. Hover, tap or scrub a chart to read a point, or focus it and use the arrow keys; legend rows toggle a series, a donut slice or a legend row selects a source (Escape clears). KPI tiles with a link lift on hover and open on Enter. Widgets with a menu have a ⋯ button labelled with their title, and View all links end their headers. Below 672px the widgets stack in one column, the KPIs go two per row (an odd last tile takes the full row), the latest invoices turn from a table into cards, and refresh and export fold into one ⋯ that opens as an action sheet. Values count up and charts draw in once; a new period swaps the numbers with a short fade instead of counting again, and reduced motion drops the counts and the chart growth.

## Installation

```bash
npx wingo-ui@latest add kpi-dashboard
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/kpi-dashboard` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/kpi-dashboard.tsx`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Bar chart](https://wingo-ui.com/components/bar-chart) ([Markdown](https://wingo-ui.com/components/bar-chart.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Date Range Picker](https://wingo-ui.com/components/date-range-picker) ([Markdown](https://wingo-ui.com/components/date-range-picker.md))
- [Donut chart](https://wingo-ui.com/components/donut-chart) ([Markdown](https://wingo-ui.com/components/donut-chart.md))
- [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) ([Markdown](https://wingo-ui.com/components/dropdown-menu.md))
- [Empty State](https://wingo-ui.com/components/empty-state) ([Markdown](https://wingo-ui.com/components/empty-state.md))
- [Funnel chart](https://wingo-ui.com/components/funnel-chart) ([Markdown](https://wingo-ui.com/components/funnel-chart.md))
- [Leaderboard](https://wingo-ui.com/components/leaderboard) ([Markdown](https://wingo-ui.com/components/leaderboard.md))
- [Line chart](https://wingo-ui.com/components/line-chart) ([Markdown](https://wingo-ui.com/components/line-chart.md))
- [Progress](https://wingo-ui.com/components/progress) ([Markdown](https://wingo-ui.com/components/progress.md))
- [Skeleton](https://wingo-ui.com/components/skeleton) ([Markdown](https://wingo-ui.com/components/skeleton.md))
- [Stat Card](https://wingo-ui.com/components/stat-card) ([Markdown](https://wingo-ui.com/components/stat-card.md))
- [Switch](https://wingo-ui.com/components/switch) ([Markdown](https://wingo-ui.com/components/switch.md))
- [Table](https://wingo-ui.com/components/table) ([Markdown](https://wingo-ui.com/components/table.md))
- [Timeline](https://wingo-ui.com/components/timeline) ([Markdown](https://wingo-ui.com/components/timeline.md))
- [Tooltip](https://wingo-ui.com/components/tooltip) ([Markdown](https://wingo-ui.com/components/tooltip.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))
- [Commerce](https://wingo-ui.com/components/commerce) ([Markdown](https://wingo-ui.com/components/commerce.md))
- [Date utils](https://wingo-ui.com/components/date-utils) ([Markdown](https://wingo-ui.com/components/date-utils.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Navigation](https://wingo-ui.com/components/navigation) ([Markdown](https://wingo-ui.com/components/navigation.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))
- [Relative time](https://wingo-ui.com/components/relative-time) ([Markdown](https://wingo-ui.com/components/relative-time.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { useState } from "react"
import { KpiDashboard, KPI_DASHBOARD_SAMPLE } from "@/components/ui/kpi-dashboard"
import type { DateRange } from "@/lib/date-utils"

export function SalesDashboard() {
  const [period, setPeriod] = useState<DateRange>({ from: new Date(2026, 8, 1), to: new Date(2026, 8, 30) })
  return (
    <KpiDashboard
      title="Dashboard"
      description="Northwind Software Inc. · Portland"
      data={KPI_DASHBOARD_SAMPLE}
      period={period}
      onPeriodChange={setPeriod}
      onRefresh={() => fetch("/api/dashboard")}
      onExport={(format) => window.open(`/api/dashboard/export?format=${format}`)}
      widgetLinks={{ invoices: "/invoices", activity: "/activity" }}
      locale="en-US"
      labels={{ invoices: "Recent invoices", viewAll: "See all" }}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `data?` | `KpiDashboardData` | `"KPI_DASHBOARD_SAMPLE"` | { kpis: KpiItem[]; revenue?: { date, value, previous? }[]; revenueBy?: { label, value }[]; sources?: { label, value, color? }[]; funnel?: { label, value }[]; leaderboard?: { id, name, avatar?, value, change? }[]; invoices?: { id, number, client, amount, issuedAt, dueAt, status, href? }[]; activity?: TimelineItemData[] }. KpiItem: { id, label, value (null shows a dash), format?, delta?, deltaFormat? (percent, number, currency, points), invertTrend?, sparkline?, sparklineType?, icon?, iconTone? (the icon tile tint), href?, goal?, info? }. A widget whose data is missing is hidden; an empty array shows its empty state. The sample (Northwind CRM, September 2026) is pinned to 2026-09-24 4:40 PM. |
| `title?` | `ReactNode` | `"Dashboard"` | The page's one <h1>: 22px on phones, 24px from 768px. |
| `description?` | `ReactNode` | `""` | A line under the title (the company, the team, a greeting). |
| `revenueByLabel?` | `string` | `""` | The bar chart's title. Empty uses labels.revenueBy ("Revenue by month"); set "Revenue by city" when the bars are cities. |
| `layout?` | `"comfortable" \| "compact"` | `"comfortable"` | comfortable: 24px gaps (16px on phones), md KPI cards and 280px charts. compact: 16px gaps, one joined panel of sm KPI tiles and 240px charts. |
| `kpiColumns?` | `number` | `0` | KPI tiles per row on wide dashboards (at most 2 on phones and 3 from 576px). 0 is auto: 2, then 3, then every tile in one row (up to 5) from 1024px. The last row always fills its width. |
| `revenueByOrientation?` | `"vertical" \| "horizontal"` | `"vertical"` | vertical columns with the latest one highlighted (months), or horizontal bars for long labels (cities, products). |
| `activityLimit?` | `number` | `5` | Activity items shown before View all. 0 shows every item. |
| `variant?` | `"surface" \| "card" \| "outline"` | `"surface"` | The surface of every widget and KPI tile: raised panels (surface), flat gray tiles (card) or hairline outlines on the page (outline). |
| `radius?` | `"none" \| "sm" \| "md" \| "lg"` | `"md"` | Corners of the widgets and KPI tiles: 0, 12, 16 or 24px. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The accent of the single-series marks: the revenue line and its area, the bars, the funnel, the salespeople's bars, the KPI icons and sparklines. Status colors (paid, overdue, deltas) keep their meaning. |
| `color?` | `string` | `""` | Any CSS color for the same marks; replaces the tone. |
| `currency?` | `string` | `"USD"` | ISO 4217 code for every amount. The block formats, it never converts: USD data with EUR prints euros. |
| `locale?` | `string` | `""` | Intl locale for numbers, money and dates (en-US: $248,300, 10/24/2026). Empty uses the UIProvider's, then the browser's. |
| `defaultComparePrevious?` | `boolean` | `true` | Starts with the previous period on: the dashed series, the KPI deltas and the salespeople's changes. comparePrevious / onComparePreviousChange control it. |
| `loading?` | `boolean` | `false` | Every KPI tile and widget in its skeleton (charts, rows, table, feed); the period picker and the switch stay usable. |
| `animated?` | `boolean` | `true` | Values count up and charts draw in once, on first view. Reduced motion turns both off on its own. |
| `animateIn?` | `boolean` | `false` | Widgets stagger in (40ms apart) when the dashboard mounts on the client. Server HTML and hydration show the final state. |
| `refreshShortcut?` | `string` | `""` | A key combo for refresh ("shift+r", "mod+shift+r"), shown in the button's tooltip and the ⋯ menu. Empty binds nothing. |
| `period?` | `DateRange` |  | The period on screen, { from, to } (controlled). The data is the app's job: the block shows the period and hands changes back. |
| `defaultPeriod?` | `DateRange` |  | The starting period while uncontrolled. Default: this month of now. |
| `onPeriodChange?` | `(period: DateRange) => void` |  | A preset or a range was applied. |
| `comparePrevious?` | `boolean` |  | Controlled compare switch. |
| `onComparePreviousChange?` | `(compare: boolean) => void` |  | The compare switch changed. |
| `widgets?` | `("revenue" \| "revenueBy" \| "sources" \| "funnel" \| "leaderboard" \| "invoices" \| "activity")[]` |  | Which widgets show and in what order (DOM order is the phone order). Default: all, in this order, each hidden when its data is missing; a widget listed here shows its empty state instead. |
| `onRefresh?` | `() => (void \| Promise<unknown>)` |  | Shows the refresh button. A returned promise spins it, shows a bar under the header and announces the time when it settles (or labels.refreshError when it rejects). |
| `onExport?` | `(format: "csv" \| "pdf") => void` |  | Shows the Export menu. Exporting is the app's job (the block does not import spreadsheet). |
| `widgetErrors?` | `Partial<Record<KpiDashboardWidgetId, ReactNode>>` |  | An error state with a Try again button in place of a widget's content (true uses the default message). |
| `onWidgetRetry?` | `(widget: KpiDashboardWidgetId) => (void \| Promise<unknown>)` |  | Try again in a widget's error state; a returned promise spins the button. |
| `widgetLinks?` | `Partial<Record<KpiDashboardWidgetId, string>>` |  | A View all link at the end of a widget's header (the invoices page, the activity log). |
| `widgetDescriptions?` | `Partial<Record<KpiDashboardWidgetId, ReactNode>>` |  | A line under a widget's title. |
| `widgetActions?` | `(widget: KpiDashboardWidgetId) => (MenuEntry[] \| undefined)` |  | Menu entries for a widget's ⋯ button, labelled "More actions for <title>"; a bottom sheet on phones. |
| `linkComponent?` | `(props: LinkComponentProps) => ReactNode` |  | Renders View all links and invoice links through your router (default: the LinkProvider's, else <a>). |
| `now?` | `Date \| number` |  | The moment the default period and the relative activity times are measured from. Default: the current time (useNow), or 24.09.2026 16:40 for the sample. |
| `labels?` | `Partial<KpiDashboardLabels>` |  | Every string: period, presets { today, last7Days, thisMonth, lastMonth, thisQuarter, thisYear }, compare, refresh, refreshing, updated(time), refreshError, export, exportCsv, exportPdf, more, moreFor(title), kpis, vsPrevious, points(value), revenue, current, previous, revenueBy, sources, total, funnel, leaderboard, invoices, activity, viewAll, statuses { paid, unpaid, overdue, draft }, number, client, status, amount, due, empty, error, retry. English defaults in DEFAULT_KPI_DASHBOARD_LABELS. |
| `classNames?` | `Partial<Record<"content" \| "header" \| "title" \| "description" \| "status" \| "controls" \| "period" \| "compare" \| "actions" \| "progress" \| "kpis" \| "kpi" \| "grid" \| "widget" \| KpiDashboardWidgetId, string>>` |  | Extra classes for the parts. Each widget's content wrapper takes its id's key. |

## Parts

### DashboardWidget (component)

The frame of every tile, for your own dashboards: a <section aria-labelledby> with an h2 (headingLevel), title, description, icon, iconTone (the icon tile's tint, default neutral), actions (MenuEntry[] for a ⋯ menu, or a node), href + linkLabel (View all), span 1 | 2 | 3 | "full" (columns of the 3 column grid; phones take the full width), rowSpan, padding "none" | "md" (charts and tables bleed), variant, radius, loading + skeleton ("chart" | "list" | "table" | "stat"), empty / emptyState, error / onRetry, footer, labels. Reads useDefaults("dashboard-widget"). Props: DashboardWidgetProps.

### KpiDashboardGrid (component)

The widget grid: one column, two from 672px and three from 1152px of its own width (container queries, so a sidebar never squeezes it). layout sets the gaps; fill (default true) stretches the last widget of a short row so no layout leaves a hole; animateIn staggers the widgets in on a client mount. Props: KpiDashboardGridProps.

### KPI_DASHBOARD_SAMPLE (constant)

Northwind CRM, September 2026 against August: $248,300 revenue (+12.4%), 64 invoices, $31,650 outstanding, 9 new clients, 23% conversion, the pipeline funnel, five salespeople, the latest invoices and eight events. Type: KpiDashboardData.

### DEFAULT_KPI_DASHBOARD_LABELS (constant)

The English strings, to spread under a translation. Type: KpiDashboardLabels.

### DEFAULT_DASHBOARD_WIDGET_LABELS (constant)

The English strings of DashboardWidget: the actions menu name, View all, the empty and error messages and Try again.

## Examples

- **Compact: KPIs and invoices**: layout="compact" joins the KPI tiles into one panel; widgets={["invoices"]} keeps a single widget.
- **Loading**: Every tile and widget in its own skeleton while the period stays usable. Press the button to swap.
- **An empty period**: A month that has not started: dashes in the tiles and an empty state in every widget.
- **A widget error with retry**: widgetErrors puts one widget in its error state; Try again resolves after a fake request.
- **Your own dashboard from the parts**: KpiDashboardGrid and DashboardWidget around a tracker, a gauge and a leaderboard for the support team.
- **On a phone (390px)**: One column, KPIs two by two, invoices as cards, refresh and export in one ⋯ action sheet.
- **Brand tone, flat tiles, cities**: tone="brand", variant="card", radius="lg" and horizontal bars for revenue by city.

## See also

- [Stat Card](https://wingo-ui.com/components/stat-card) ([Markdown](https://wingo-ui.com/components/stat-card.md))
- [Line chart](https://wingo-ui.com/components/line-chart) ([Markdown](https://wingo-ui.com/components/line-chart.md))
- [Leaderboard](https://wingo-ui.com/components/leaderboard) ([Markdown](https://wingo-ui.com/components/leaderboard.md))
- [Timeline](https://wingo-ui.com/components/timeline) ([Markdown](https://wingo-ui.com/components/timeline.md))
- [Table](https://wingo-ui.com/components/table) ([Markdown](https://wingo-ui.com/components/table.md))
- [Admin Dashboard Page](https://wingo-ui.com/components/admin-dashboard-page) ([Markdown](https://wingo-ui.com/components/admin-dashboard-page.md))

---

Canonical: https://wingo-ui.com/components/kpi-dashboard
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
