Widgets
A library of 20+ ready-made dashboard widgets — KPI tiles in eight styles, chart.js donut/gauge/bar cards, a Recharts area-trend card, welcome heroes, activity/notification/timeline/people lists, a rich report panel, a mini calendar, and Google-Cloud / Firebase console-style sets — all showcased on the /widgets gallery and importable one-by-one from src/components/widgets/.
Overview
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 side-by-side), a KPI tile gallery (one representative tile per style), Chart cards
(donut, gauge, breakdown, area trend, stat list, and four weekly bar cards), Overview panels (the
full-width report panel + mini calendar), Lists & activity, and Cloud & infrastructure (the GCP
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, …) orcurrentColor; the canvas charts (chart.js) can't read CSS variables, so they get their colors fromuseChartTokens(), 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
widgetsnamespace (src/locales/<lng>/widgets.json, 214 keys, en + ja at parity); the components themselves take already-translated strings as props (onlyWidgetMenutranslates its own items). - Widget headers are borderless.
WidgetHeadersits inside the card's own padding with no divider band — the modern portlet look — and renders the⋯WidgetMenuby default (menu={false}opts out).
Architecture & files
| File | Responsibility |
|---|---|
src/pages/ui/WidgetsPage.tsx | The /widgets gallery — composes every widget with demo data, i18n'd section headings, and the KPI_GALLERY style catalog. |
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 (Refresh / Export / Details / Fullscreen / Hide) 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 used across all widgets (sign drives arrow + success/danger tint). |
src/components/widgets/primitives/Sparkline.tsx | Dependency-free SVG sparkline (line/area/bar) with an animated left→right clip-rect reveal; colored via currentColor. |
src/components/widgets/primitives/MiniBarChart.tsx | Weekly highlighted bar chart (react-chartjs-2 Bar) — one solid bar, the rest at ~35% alpha, staggered grow-in. |
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, per-segment share rows, and footer stats. |
src/components/widgets/GaugeWidget.tsx | Segmented half-doughnut gauge (chart.js) + tone banner + optional sub-score bars + bottom stat tiles. |
src/components/widgets/AreaTrendWidget.tsx | Recharts area chart with a period Dropdown, headline value, breakdown rows, and footer stats. |
src/components/widgets/BarStatWidget.tsx | Headline value + trend over a MiniBarChart with locale-aware weekday labels (Intl, no i18n keys). |
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 block, featured stats, highlight chips, team progress bars, monthly-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; skin-aware GcpHue accents. |
src/components/widgets/firebase/* (2 cards) | Firebase-console set: metric tile + plan-usage card with a primary-gradient header. |
src/locales/en/widgets.json (+ ja/…) | The widgets i18n namespace — 214 keys, all showcase labels. |
src/hooks/useChartTokens.ts | Reads the raw token vars for canvas charts; builds the categorical series palette (Prism's --c1..--c6 when present, else hue-rotation around --primary). |
The route is code-split:
src/app/[locale]/(app)/widgets/page.tsx ← routes are code-split per page by defaultUsage
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 {Stagger} from '@/components/motion'
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; outside a Stagger, a widget simply renders
without the entrance cascade. 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, i18n'd via widgets:menu*) 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 (grid column classes for the Stagger body),
children.
TrendPill — value: number (signed percentage; sign drives the arrow and the success/danger
tint), suffix?: string (trailing context like "vs last week"), 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, so it re-skins without useChartTokens. |
width / height | number | 120 / 40 | SVG viewBox only — the element scales to its container via h-full w-full. |
The line/area draw-in is an animated clip rect whose width grows from x=0 — strictly
left→right, robust under preserveAspectRatio="none" (a stroke-dash pathLength would render
partially there). Under reduced motion the full curve renders immediately.
MiniBarChart — data: number[], labels: string[], highlightIndex: number (the one bar drawn
at full opacity; the rest use the same token at ~35% alpha via #rrggbbaa), tone? (default
'primary'). A chart.js Bar (registers BarElement/CategoryScale/LinearScale/Tooltip) 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 (icon+label front, gradient value back).
Chart cards
DonutWidget — title, subtitle, total: number, totalLabel, prefix?,
segments: DonutSegment[] ({label, value} — colored by cycling useChartTokens().series),
footer?: {label; value; delta?}[], tint? (soft-tinted surface + uppercase tinted header), icon?.
chart.js Doughnut, cutout: '68%', centered AnimatedNumber total, per-segment share rows with
proportional bars.
GaugeWidget — title, subtitle, value (0–100), valueLabel,
tone?: 'primary' | 'success' | 'warning' | 'info' (default primary), segments? (arc tick count,
default 15), bannerIcon: LucideIcon, bannerText, stats: {label; value; delta?}[] (bottom stat
tiles), breakdown?: {label; value}[] (labelled sub-score bars). A chart.js half-doughnut
(rotation: -90, circumference: 180) of rounded ticks filled to the value.
AreaTrendWidget — title, subtitle, calloutLabel,
periods: TrendPeriod[] ({key, label, change, data: {x; y}[], value?} — a period Dropdown in the
header switches the chart and headline), breakdownLabel?, breakdown?: {label; value; delta?}[],
footer?: {label; value}[]. The one Recharts widget (gradient AreaChart); axis/tooltip/stroke colors
from useChartTokens().
BarStatWidget — title, subtitle, value: string, delta: number, data: number[] (7
values), highlightIndex, tone?, breakdownLabel?, breakdown?: {label; value; delta?}[],
stats?: {label; value}[]. Weekday x-labels are generated with Intl.DateTimeFormat from the active
language — dates never get i18n keys.
BreakdownWidget — title, subtitle, rows: BreakdownRow[] ({label, value, tone, delta?};
tone includes neutral), total? (defaults to the row sum), unit?, delta?, activity?: number[]
(0–100 mini activity strip), activityLabel?, footer?: {label; value}[], tint?, icon?. Stacked
proportion bar + legend, all DOM/token-based (no canvas).
StatListChartWidget — title, subtitle, rows: StatRow[]
({icon, tone, label, sublabel, amount, delta}), spark: number[], sparkTone?, sparkLabel?,
footer?: {label; value}[].
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? (count label — pass a translated one; the fallback is literal 'tasks'). |
Composite widgets
WelcomeWidget — greeting, name, message, ctaLabel, onCta?, avatar?,
variant?: 'gradient' | 'panel' | 'lines' (default gradient), dateLabel? (chip line),
stats?: {label; value}[] (3-up mini-stat row). All three variants share one vertical layout (chip ·
avatar+greeting · message · stats · CTA) for consistent spacing and equal height — only the
background/text treatment differs. lines layers a HeroCanvas variant="lines" (tones
['primary', 'info']) under a surface gradient scrim; gradient is a primary gradient with
text-primary-foreground.
ReportPanelWidget — title, subtitle, revenueLabel, revenue: number, revenuePrefix?,
revenueDelta, compareLabel, stats: ReportStat[] ({label, value, delta} featured tiles),
highlights?: {label; value; delta?}[] (chip strip), teamsLabel, teams: TeamBar[]
({name, avatar?, value, percent} — ranked Progress bars), goalLabel? + goalPercent? +
goalCaption? (monthly-goal Progress footer).
MiniCalendarWidget — title, subtitle, events: CalEvent[] ({date: 'YYYY-MM-DD', title, time, tone} — dates dot the calendar, the first 3 list as "upcoming"), upcomingLabel?. Reuses the
calendar app's MiniCalendar with the active i18n.language.
Google Cloud & Firebase sets
Accents in these sets use the GcpHue = 'primary' | 'info' type from
src/components/widgets/gcp/hues.ts — see the skin rule.
| Component | Key props |
|---|---|
GcpServiceTile | icon, name, hue: GcpHue, value: number (AnimatedNumber), unit, statusLabel, healthy? (default true; drives the primary/info status dot). |
GcpUsageMeter | title, subtitle, rows: QuotaRow[] ({label, used, limit, unit, hue} meters), footer?: {label; value}[]. |
GcpBillingCard | label, amount: number, delta, projectLabel + project, creditLabel + credit, breakdownLabel? + breakdown?: SpendRow[] ({label, value, pct} — bars cycle primary → info), forecastLabel? + forecast?. Top accent bar is a primary→info gradient. |
GcpStatusCard | title, subtitle, services: ServiceStatus[] ({name, region, health: 'ok' | 'warn' | 'down', healthLabel} — ok/warn map to primary/info so they re-skin; only down keeps the universal danger red). |
FirebaseMetricTile | icon, name, value: number, unit?, delta?, prefix?. |
FirebaseUsageCard | title, planLabel, rows: FirebaseUsageRow[] ({label, used, limit, unit} — bars + per-row remaining caption), remainingLabel?, footer?: {label; value}[]. 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 (a footer stat row, a breakdown, an activity strip) 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()
(src/hooks/useChartTokens.ts), 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 when the user switches theme or skin. The Recharts AreaTrendWidget uses the
same hook (Recharts takes literal color strings too). The SVG Sparkline doesn't need it — it draws
with currentColor from a tone text class. Canvas animations (donut rotate-in, gauge fill, bar
grow-in) are all gated by useReducedMotion(). See Charts for the full
useChartTokens story.
The skin rule: adaptive accents are primary/info only
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 inAreaTrendWidget/BarStatWidget/GcpBillingCard, the Firebase gradient.GcpStatusCardmapsok/warnto primary/info for the same reason; 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 — see Design skins); otherwise it anchors on the exact primary and hue-rotates around it with normalized saturation/lightness, so it stays distinct on every skin (including mono Graphite) and in dark mode.
Text & i18n
The showcase's labels live in src/locales/<lng>/widgets.json (en + ja at parity; other languages via
npm run i18n:translate). Widgets take translated strings as props — when you add a widget instance,
add its labels to the widgets namespace and pass t('widgets:…'). Only WidgetMenu translates
internally (menuLabel/menuRefresh/menuExport/menuDetails/menuFullscreen/menuHide/menuToast).
Dates (the hero date chip, BarStatWidget weekdays, MiniCalendarWidget) come from Intl with the
active language — never i18n keys.
Examples
A KPI row (one line per tile)
<Stagger className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
<KpiTile variant="flat" tone="primary" icon={DollarSign} label={t('mRevenue')} value={48250} prefix="$" delta={12.4} spark={spark} />
<KpiTile variant="chip" tone="info" icon={Activity} label={t('mActive')} value={128} delta={6.4} />
<KpiTile variant="gradient" tone="success" icon={TrendingUp} label={t('mSuccess')} value={4.6} suffix="%" decimals={1} delta={2.1} spark={spark} />
<KpiTile variant="backgroundIcon" tone="danger" icon={Flame} label={t('kpiBounce')} value={32.1} suffix="%" decimals={1} delta={-4.2} />
</Stagger>A gauge with sub-scores and bottom stats
<GaugeWidget
title={t('gaugeTitle')}
subtitle={t('gaugeSubtitle')}
value={84}
valueLabel={t('gaugeValueLabel')}
tone="primary"
bannerIcon={BadgeCheck}
bannerText={t('gaugeBanner')}
stats={[
{label: t('gaugeOnTime'), value: '92%', delta: 3.1},
{label: t('gaugeRating'), value: '4.8', delta: 1.4},
]}
breakdown={[
{label: t('gaugeSpeed'), value: 88},
{label: t('gaugeReliability'), value: 94},
]}
/>A custom header action instead of the demo ⋯ menu
<WidgetHeader
title={t('trendTitle')}
subtitle={t('trendSubtitle')}
menu={false}
action={<Button variant="outline" size="sm">{t('period7d')}</Button>}
/>Best practices
- Keep the flex-fill chain intact when composing or cloning a widget —
h-fullwrapper →flex h-full flex-colcard →flex flex-1 flex-colbody →mt-autofooter. Dropping any link reintroduces the dead-gap problem. - Canvas colors only via
useChartTokens(). Never pass hex literals to chart.js/Recharts — they won't follow theme or skin changes. - Respect the skin rule. New decorative accents:
primary/info. New status semantics:success/warning/danger. Multi-hue series:tk.series. - Use
text-*-foregroundon solid fills, nevertext-white— Amber and Bento define dark foregrounds. - Pass translated strings. Widgets are i18n-agnostic by design; the caller owns
t(). Add keys to thewidgetsnamespace (en + ja) for anything user-facing. - Render widgets inside a
Stagger(orWidgetSection) so the built-instaggerItementrance actually plays; gate any new imperative animation onuseReducedMotion(). - Prefer real content over whitespace — use the optional
footer/breakdown/activity/statsprops to balance card heights in a row.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
| A chart keeps its old colors after a theme/skin switch | The color was hardcoded instead of read from useChartTokens() — the hook re-reads on class/data-skin/data-oled mutations; literals never update. |
| Cards in a row have uneven heights / bottom gaps | A link in the flex-fill chain is missing (h-full → flex h-full flex-col → flex flex-1 flex-col → mt-auto footer), or the row isn't a stretching grid. |
| Widgets don't animate in | They must sit inside a Stagger (e.g. via WidgetSection) — the staggerItem variants only run under a stagger container. 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". Don't add a transform-origin-based reveal. |
| The ⋯ menu shows on a widget where it doesn't belong | WidgetHeader renders WidgetMenu by default — pass menu={false}. |
| The ⋯ menu actions "do nothing" real | By design — WidgetMenu is presentational (toast per action). Wire a real menu via the action slot + 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). |
| Gradient KPI text is unreadable on the Amber skin | Something used text-white instead of the FG map (text-primary-foreground etc.). Use the token foreground. |
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 (no divider, no collapse
toolbar).
Which chart library do they use? chart.js (react-chartjs-2) for the donut, gauge, and weekly bars
— registering only the elements each needs — plus Recharts for AreaTrendWidget and a dependency-free
SVG Sparkline. The dedicated chart showcases live under /charts (see
Charts).
Do widgets fetch data? No — they are pure presentational components. Pass values from your API
layer (see src/lib/api.ts) or state.
Can I use a widget outside the showcase? Yes — everything is exported from
@/components/widgets. The home dashboards compose similar patterns; the showcase is the reference
catalog.
Why does the demo menu export nothing? The showcase is a template gallery — WidgetMenu proves
the interaction pattern (portal, separator, danger tone) and fires toasts; production apps replace it.
Notes for designers & content editors
- All showcase copy lives in
src/locales/<lng>/widgets.json— edit labels there, never in components. Names of people/products in the demo data are literal by convention. - Colors re-skin automatically. Every widget follows the token system; switching skin or dark mode re-colors tiles, charts, gauges, and the GCP/Firebase accents with zero per-widget design work.
- Widget headers are deliberately borderless — no divider band. Keep titles short (they truncate) and subtitles to a terse descriptor, matching the Panel subtitle convention.
- The welcome heroes share one layout across all three variants so a row of them lines up; pick a variant by mood (vivid gradient · quiet panel · animated lines), not by structure.
- Trend pills always color by sign (green up / red down) — don't request inverted semantics per widget.
Related
Panel
the general collapsible portlet; widgets use their own lighter header instead
Core & feedback components
Card, Badge, Avatar, Progress, Toast used inside the widgets
Overlays & disclosure
the Dropdown behind WidgetMenu and the period picker
Charts
the full chart showcases and the useChartTokens() contract
Animation & effects
Stagger/staggerItem, AnimatedNumber, HeroCanvas, and the reduced-motion policy
Design skins
why adaptive accents stay on primary/info and how Prism's --c1..--c6 palette feeds tk.series
Design tokens & dark mode
the token vars everything reads
Dashboards
the home dashboards that compose similar widget patterns
Architecture & routing
how the lazy /widgets route is wired
Was this page helpful?
