KPI Dashboard React block for Tailwind CSS v4
v1.0.1Updated in 1.2.0
A React KPI dashboard: metric tiles with deltas, revenue against the last period, sources, funnel, top sellers, invoices and activity.
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.
KPI Dashboard is a Pro React block in the React blocks category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add kpi-dashboard, together with Badge, Bar chart, Button, Date Range Picker, Donut chart and 23 more and the npm packages motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add kpi-dashboardKPI Dashboard is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import KpiDashboard from @/components/ui/kpi-dashboard.
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
datadefault"KPI_DASHBOARD_SAMPLE"KpiDashboardData{ 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.
titledefault"Dashboard"ReactNodeThe page's one <h1>: 22px on phones, 24px from 768px.
descriptiondefault""ReactNodeA line under the title (the company, the team, a greeting).
revenueByLabeldefault""stringThe bar chart's title. Empty uses labels.revenueBy ("Revenue by month"); set "Revenue by city" when the bars are cities.
layoutdefault"comfortable""comfortable" | "compact"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.
kpiColumnsdefault0numberKPI 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.
revenueByOrientationdefault"vertical""vertical" | "horizontal"vertical columns with the latest one highlighted (months), or horizontal bars for long labels (cities, products).
activityLimitdefault5numberActivity items shown before View all. 0 shows every item.
variantdefault"surface""surface" | "card" | "outline"The surface of every widget and KPI tile: raised panels (surface), flat gray tiles (card) or hairline outlines on the page (outline).
radiusdefault"md""none" | "sm" | "md" | "lg"Corners of the widgets and KPI tiles: 0, 12, 16 or 24px.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"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.
colordefault""stringAny CSS color for the same marks; replaces the tone.
currencydefault"USD"stringISO 4217 code for every amount. The block formats, it never converts: USD data with EUR prints euros.
localedefault""stringIntl locale for numbers, money and dates (en-US: $248,300, 10/24/2026). Empty uses the UIProvider's, then the browser's.
defaultComparePreviousdefaulttruebooleanStarts with the previous period on: the dashed series, the KPI deltas and the salespeople's changes. comparePrevious / onComparePreviousChange control it.
loadingdefaultfalsebooleanEvery KPI tile and widget in its skeleton (charts, rows, table, feed); the period picker and the switch stay usable.
animateddefaulttruebooleanValues count up and charts draw in once, on first view. Reduced motion turns both off on its own.
animateIndefaultfalsebooleanWidgets stagger in (40ms apart) when the dashboard mounts on the client. Server HTML and hydration show the final state.
refreshShortcutdefault""stringA key combo for refresh ("shift+r", "mod+shift+r"), shown in the button's tooltip and the ⋯ menu. Empty binds nothing.
periodDateRangeThe period on screen, { from, to } (controlled). The data is the app's job: the block shows the period and hands changes back.
defaultPeriodDateRangeThe starting period while uncontrolled. Default: this month of now.
onPeriodChange(period: DateRange) => voidA preset or a range was applied.
comparePreviousbooleanControlled compare switch.
onComparePreviousChange(compare: boolean) => voidThe 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") => voidShows the Export menu. Exporting is the app's job (the block does not import spreadsheet).
widgetErrorsPartial<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.
widgetLinksPartial<Record<KpiDashboardWidgetId, string>>A View all link at the end of a widget's header (the invoices page, the activity log).
widgetDescriptionsPartial<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) => ReactNodeRenders View all links and invoice links through your router (default: the LinkProvider's, else <a>).
nowDate | numberThe 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.
labelsPartial<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.
classNamesPartial<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
componentThe 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
componentThe 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
constantNorthwind 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
constantThe English strings, to spread under a translation. Type: KpiDashboardLabels.
DEFAULT_DASHBOARD_WIDGET_LABELS
constantThe 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.
Dependencies
Files
- components/ui/kpi-dashboard.tsx
FAQ
Is the React KPI Dashboard block free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install KPI Dashboard?
Run npx wingo-ui@latest add kpi-dashboard. It also adds the Wingo UI items it builds on (Badge, Bar chart, Button, Date Range Picker, Donut chart and 23 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/kpi-dashboard.
How does KPI Dashboard behave on phones?
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.
See also
More React blocks
Articles using this component
- Shadcn KPI Cards: Deltas, Sparklines and Goal Bars
Component guides9 min read
- Nextjs Admin Dashboard Tutorial: 5 Screens, Phone-Ready
Build it: screens and apps23 min read