Widgets
A library of 20+ ready-made dashboard widgets — KPI tiles, chart cards, feeds, heroes, and console-style sets — all token-driven and importable one-by-one.
The Widgets showcase (/widgets, sidebar Components → Widget, lazy-loaded) is a modern, interactive
gallery of every widget in src/components/widgets/, arranged in labelled sections: Welcome heroes
(three variants), a KPI tile gallery, Chart cards (donut, gauge, breakdown, area trend, stat
list, weekly bars), Overview panels (report panel + mini calendar), Lists & activity, and
Cloud & infrastructure (Google Cloud and Firebase console-style sets).
Every widget is fully data-driven via props — the page passes demo data, but the components carry no
sample content of their own. Widgets are pure presentational cards: they render inside the shared Card
(or their own token-styled shell), wrap themselves in a staggerItem motion variant so a surrounding
Stagger grid cascades them in, and stay reduced-motion safe throughout.
Three cross-cutting rules apply everywhere:
Colors are tokens only
DOM/SVG widgets use semantic utilities (bg-primary, text-success, …) or currentColor; the
canvas charts (chart.js) can't read CSS variables, so they get their colors from
useChartTokens(), which reads the raw --* vars and re-reads on theme/skin change.
User-facing text is i18n keys
The page's labels live in the widgets namespace (214 keys, en + ja at parity); the components
themselves take already-translated strings as props (only WidgetMenu translates its own items).
Widget headers are borderless
WidgetHeader sits inside the card's own padding with no divider band — the modern portlet look —
and renders the ⋯ WidgetMenu by default (menu={false} opts out).
Architecture & files
| File | Responsibility |
|---|---|
src/pages/ui/WidgetsPage.tsx | The /widgets gallery — composes every widget with demo data and i18n'd section headings. |
src/components/widgets/index.ts | The barrel — import every widget and its exported row types from @/components/widgets. |
src/components/widgets/primitives/WidgetHeader.tsx | Borderless card header (title · subtitle · optional icon · action slot · ⋯ menu by default). |
src/components/widgets/primitives/WidgetMenu.tsx | The ⋯ overflow Dropdown — presentational demo actions that fire toasts. |
src/components/widgets/primitives/WidgetSection.tsx | Labelled page section — icon-chip heading + description + a Stagger responsive grid. |
src/components/widgets/primitives/TrendPill.tsx | The up/down percentage chip (sign drives arrow + success/danger tint). |
src/components/widgets/primitives/Sparkline.tsx | Dependency-free SVG sparkline with an animated left→right clip-rect reveal; currentColor. |
src/components/widgets/primitives/MiniBarChart.tsx | Weekly highlighted bar chart (react-chartjs-2 Bar) — one solid bar, the rest ~35% alpha. |
src/components/widgets/KpiTile.tsx | The KPI stat tile with eight variant styles. |
src/components/widgets/DonutWidget.tsx | chart.js Doughnut with a centered animated total, share rows, footer stats. |
src/components/widgets/GaugeWidget.tsx | Segmented half-doughnut gauge (chart.js) + tone banner + sub-score bars + bottom stat tiles. |
src/components/widgets/AreaTrendWidget.tsx | Recharts area chart with a period Dropdown, headline value, breakdown rows, footer stats. |
src/components/widgets/BarStatWidget.tsx | Headline value + trend over a MiniBarChart with locale-aware weekday labels. |
src/components/widgets/BreakdownWidget.tsx | Stacked proportion bar + legend + optional 12-bar activity strip + footer stats. |
src/components/widgets/StatListChartWidget.tsx | Icon stat rows over a trend sparkline, with footer stats. |
src/components/widgets/ActivityFeedWidget.tsx | Activity feed — colored icon rows with title, detail, timestamp. |
src/components/widgets/NotificationsWidget.tsx | Avatar notification rows with unread dots and a count Badge. |
src/components/widgets/TimelineWidget.tsx | Vertical timeline — time, colored node, title + description. |
src/components/widgets/PeopleListWidget.tsx | People list — avatar + presence, badge, count/rating meta. |
src/components/widgets/WelcomeWidget.tsx | Greeting hero in three variants (gradient/panel/lines) sharing one vertical layout. |
src/components/widgets/ReportPanelWidget.tsx | Rich full-width report — headline revenue, featured stats, highlight chips, team bars, goal footer. |
src/components/widgets/MiniCalendarWidget.tsx | Month mini-calendar (reuses the calendar app's MiniCalendar) + upcoming-events list. |
src/components/widgets/gcp/* (hues.ts + 4 cards) | Google-Cloud-console set: service tile, quota meter, billing card, status list. |
src/components/widgets/firebase/* (2 cards) | Firebase-console set: metric tile + plan-usage card with a primary-gradient header. |
src/hooks/useChartTokens.ts | Reads the raw token vars for canvas charts; builds the categorical series palette. |
The route is registered lazily in src/routes.tsx:
const WidgetsPage = lazy(() => import('@/pages/ui/WidgetsPage').then((m) => ({default: m.WidgetsPage})))
// …
{path: '/widgets', element: <WidgetsPage />},Usage
Import from the barrel and drop widgets into a Stagger grid (each widget carries its own staggerItem
variant, so the grid cascades them in automatically):
import {KpiTile, DonutWidget, WidgetSection} from '@/components/widgets'
import {DollarSign} from 'lucide-react'
<WidgetSection title={t('chartsTitle')} subtitle={t('chartsSubtitle')} gridClassName="lg:grid-cols-3">
<KpiTile variant="flat" tone="primary" icon={DollarSign} label={t('mRevenue')} value={48250} prefix="$" delta={12.4} spark={[12, 18, 14, 22, 19, 27, 24, 31]} />
<DonutWidget
title={t('donutTitle')}
subtitle={t('donutSubtitle')}
total={4820}
totalLabel={t('donutTotal')}
segments={[{label: t('segCity'), value: 2100}, {label: t('segAirport'), value: 1450}]}
/>
</WidgetSection>WidgetSection provides the heading and the Stagger grid. All widget text arrives as props — pass
t(…) results, never literals.
API / Props
All widgets share the pattern: an outer motion.div with variants={staggerItem} and h-full, a
Card/token-styled shell, a WidgetHeader, and (where a footer exists) an mt-auto bottom-anchored
stat row. Tones are the five semantic keys primary | success | warning | danger | info unless noted.
Primitives
WidgetHeader
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | — | The card heading (truncated). |
subtitle | ReactNode | — | Small descriptor under the title. |
action | ReactNode | — | Right-aligned slot (filter, badge, "view all"…), shown before the ⋯ menu. |
menu | boolean | true | Render the WidgetMenu ⋯ overflow. Pass menu={false} to opt out. |
icon | ReactNode | — | Leading node before the title (e.g. a 3D illustration <img>). |
tone | Tone | — | When set, the title renders as an uppercase tinted label (the "engage" look). |
className | string | — | Extra classes on the header row. |
WidgetMenu takes no props — it is the demo ⋯ Dropdown (Refresh / Export / View details /
Fullscreen / Hide) whose items fire an info toast. Replace it with a real menu by passing your own
action and menu={false} to WidgetHeader.
WidgetSection — title: string, subtitle: string, icon?: ReactNode (rendered in a
primary-tinted chip), gridClassName?: string, children.
TrendPill — value: number (signed percentage; sign drives the arrow and the success/danger
tint), suffix?: string, className?.
Sparkline
| Prop | Type | Default | Description |
|---|---|---|---|
data | number[] | — | The series (auto-scaled to min/max). |
variant | 'line' | 'area' | 'bar' | 'area' | Smoothed Catmull-Rom curve (line/area) or baseline-growing bars. |
tone | Tone | 'primary' | Sets the text-color class; the SVG draws with currentColor. |
width / height | number | 120 / 40 | SVG viewBox only — the element scales to its container. |
The line/area draw-in is an animated clip rect whose width grows from x=0 — strictly left→right,
robust under preserveAspectRatio="none". Under reduced motion the full curve renders immediately.
MiniBarChart — data: number[], labels: string[], highlightIndex: number (the one bar drawn
at full opacity; the rest at ~35% alpha), tone? (default 'primary'). A chart.js Bar with a
staggered grow-in, disabled under reduced motion; colors from useChartTokens().
KPI tiles
KpiTile (KpiTileProps, exported)
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Metric name. |
value | number | — | Count-up headline via AnimatedNumber. |
icon | LucideIcon | — | Metric icon. |
variant | KpiVariant | 'flat' | flat | minimal | gradient | glass | outline | chip | backgroundIcon | flip. |
tone | Tone | 'primary' | Accent tone for icon chip / gradient / dot. |
delta | number | — | Renders a TrendPill. |
hint | string | — | TrendPill suffix (e.g. "vs last week"). |
spark | number[] | — | Renders an inline Sparkline (variant-dependent placement). |
prefix / suffix / decimals | string / string / number | — | Passed through to AnimatedNumber. |
tint | boolean | false | Soft-tinted card surface — flat variant only. |
Solid-fill variants (gradient, the flip back face) use the text-*-foreground token map — never
text-white — so contrast holds on skins with dark foregrounds (e.g. Amber). flip is a CSS 3D
hover-flip.
Chart cards
DonutWidget—title,subtitle,total: number,totalLabel,prefix?,segments: DonutSegment[]({label, value}— colored by cyclinguseChartTokens().series),footer?,tint?,icon?. chart.jsDoughnut,cutout: '68%', centeredAnimatedNumbertotal.GaugeWidget—title,subtitle,value(0–100),valueLabel,tone?: 'primary' | 'success' | 'warning' | 'info',segments?(arc tick count, default15),bannerIcon,bannerText,stats,breakdown?. A chart.js half-doughnut of rounded ticks.AreaTrendWidget—title,subtitle,calloutLabel,periods: TrendPeriod[](a periodDropdownswitches the chart and headline),breakdownLabel?,breakdown?,footer?. The one Recharts widget; colors fromuseChartTokens().BarStatWidget—title,subtitle,value: string,delta: number,data: number[](7 values),highlightIndex,tone?,breakdown?,stats?. Weekday labels viaIntl— no i18n keys.BreakdownWidget—title,subtitle,rows: BreakdownRow[](tone includesneutral),total?,unit?,delta?,activity?: number[],activityLabel?,footer?,tint?,icon?. DOM/token-only.StatListChartWidget—title,subtitle,rows: StatRow[],spark: number[],sparkTone?,sparkLabel?,footer?.
Lists & feeds
| Component | Key props |
|---|---|
ActivityFeedWidget | title, subtitle, items: ActivityItem[] ({icon, tone, title, text, time}), action?. |
NotificationsWidget | title, subtitle, items: NotificationItem[] ({name, avatar?, text, time, unread?}), count? (header Badge). |
TimelineWidget | title, subtitle, items: TimelineItem[] ({time, tone, title, desc}). |
PeopleListWidget | title, subtitle, people: Person[] ({name, avatar?, status?, subtitle, badge?, time?, count?, rating?}), action?, compact?, unit?. |
Composite widgets
WelcomeWidget—greeting,name,message,ctaLabel,onCta?,avatar?,variant?: 'gradient' | 'panel' | 'lines'(defaultgradient),dateLabel?,stats?. All three variants share one vertical layout (chip · avatar+greeting · message · stats · CTA) for consistent spacing and equal height.lineslayers aHeroCanvas variant="lines"under a surface gradient scrim.ReportPanelWidget—title,subtitle,revenueLabel,revenue: number,revenuePrefix?,revenueDelta,compareLabel,stats: ReportStat[],highlights?,teamsLabel,teams: TeamBar[](rankedProgressbars),goalLabel?+goalPercent?+goalCaption?.MiniCalendarWidget—title,subtitle,events: CalEvent[]({date, title, time, tone}— dates dot the calendar, the first 3 list as "upcoming"),upcomingLabel?. Reuses the calendar app'sMiniCalendarwith the activei18n.language.
Google Cloud & Firebase sets
Accents in these sets use the GcpHue = 'primary' | 'info' type — see the skin rule.
| Component | Key props |
|---|---|
GcpServiceTile | icon, name, hue: GcpHue, value: number, unit, statusLabel, healthy? (drives the primary/info status dot). |
GcpUsageMeter | title, subtitle, rows: QuotaRow[] ({label, used, limit, unit, hue} meters), footer?. |
GcpBillingCard | label, amount: number, delta, projectLabel + project, creditLabel + credit, breakdown? (bars cycle primary → info), forecast?. |
GcpStatusCard | title, subtitle, services: ServiceStatus[] ({name, region, health: 'ok' | 'warn' | 'down', healthLabel} — ok/warn map to primary/info; only down keeps danger red). |
FirebaseMetricTile | icon, name, value: number, unit?, delta?, prefix?. |
FirebaseUsageCard | title, planLabel, rows: FirebaseUsageRow[], remainingLabel?, footer?. Primary-gradient header with text-primary-foreground. |
Configuration & customization
The equal-height flex-fill chain
Grid cells stretch, but card content top-aligns — so a short card in a row of tall ones would show a dead gap at the bottom. Every widget therefore builds a flex-fill chain and bottom-anchors its footer:
<motion.div variants={staggerItem} className="h-full"> {/* the grid cell fills */}
<Card className="flex h-full flex-col"> {/* the card fills the cell */}
<CardBody className="flex flex-1 flex-col"> {/* the body absorbs the height */}
<WidgetHeader … />
{/* …content; an inner chart area can take flex-1 too… */}
<div className="mt-auto grid … border-t border-border pt-4">{/* footer pins to the bottom */}</div>
</CardBody>
</Card>
</motion.div>Follow this chain in any new widget: h-full wrapper → Card flex h-full flex-col →
CardBody flex flex-1 flex-col → mt-auto on the footer/stat row. Where a card still reads short,
add real content rather than leaving whitespace — that's why most widgets accept optional
footer/breakdown/activity props.
Chart colors & useChartTokens()
The chart.js widgets (DonutWidget, GaugeWidget, MiniBarChart/BarStatWidget) render to a
<canvas>, which cannot resolve CSS variables — so they call useChartTokens(), which reads the raw
--primary/--info/--foreground/… values off <html> and re-reads via a MutationObserver on
class/data-skin/data-oled. Every canvas chart therefore re-colors live on theme/skin switch. The
Recharts AreaTrendWidget uses the same hook. The SVG Sparkline doesn't need it — it draws with
currentColor. Canvas animations are all gated by useReducedMotion(). See
Charts for the full useChartTokens story.
The skin rule
Color skins override only --primary and --info — not --success/--warning/--danger, which
stay semantic. So:
- Brand/decorative accents that should re-skin use only
primaryandinfo— theGcpHuetype, the alternating breakdown dots, the Firebase gradient.GcpStatusCardmapsok/warnto primary/info; only a harddownkeeps danger red. - True status meaning (TrendPill up/down, health
down, warning banners) keepssuccess/warning/danger. - The multi-hue categorical palette (
useChartTokens().series, used byDonutWidget) prefers a skin-provided--c1..--c6palette (the Prism skin defines one); otherwise it anchors on the exact primary and hue-rotates around it so it stays distinct on every skin and in dark mode.
Use text-*-foreground on solid fills
Never text-white — Amber and Bento define dark foregrounds. Using the token foreground keeps gradient
KPI text readable across skins.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
| A chart keeps its old colors after a theme/skin switch | The color was hardcoded instead of read from useChartTokens() — literals never update. |
| Cards in a row have uneven heights / bottom gaps | A link in the flex-fill chain is missing, or the row isn't a stretching grid. |
| Widgets don't animate in | They must sit inside a Stagger (e.g. via WidgetSection). Reduced motion also disables entrances. |
| The sparkline reveals from the center or renders partially | Keep the animated clip-rect reveal — a stroke-dash pathLength breaks under preserveAspectRatio="none". |
| The ⋯ menu shows where it doesn't belong | WidgetHeader renders WidgetMenu by default — pass menu={false}. |
| A GCP/Firebase accent doesn't change with the skin | It used success/warning/danger — skins don't override those. Use primary/info (GcpHue). |
FAQ
Are these the same as StatCard or Panel? No. StatCard is a single dashboard stat primitive and
Panel is the general collapsible portlet. The widgets library is a catalog of purpose-built dashboard
cards with their own borderless WidgetHeader.
Which chart library do they use? chart.js (react-chartjs-2) for the donut, gauge, and weekly bars,
plus Recharts for AreaTrendWidget and a dependency-free SVG Sparkline.
Do widgets fetch data? No — they are pure presentational components. Pass values from your API layer or state.
Can I use a widget outside the showcase? Yes — everything is exported from @/components/widgets. The
home dashboards compose similar patterns.
Related
Components
The core primitives (Card, Badge, Progress) widgets compose with.
Charts
The full chart showcases and the useChartTokens contract.
Dashboards
The home dashboards that compose similar widget patterns.
Design skins
Why adaptive accents stay on primary/info and how Prism feeds tk.series.
Design tokens & dark mode
The token vars everything reads.
Was this page helpful?
