One question per screen. Hierarchy: KPI → trend → detail. A dashboard without a clear decision or action is decoration.
Principles hold on any front end. Examples and composition in React. Visual/tokens: better-web-interface and design-system. Components: react-component-engineering. API/BFF contracts: api-design.
Before designing
- Audience — ops/admin (act now) vs business/BI (compare and understand).
- Decisions — what does the person do after looking? (investigate, approve, escalate, export).
- Cadence — real time, hourly, daily; this defines freshness and auto-refresh.
- Heroes — 1–3 primary metrics; the rest is support or drill-down.
- Inventory — reuse the shell, filter bar, and widgets from the design system; don't invent a second visual language.
Anatomy
Shell (nav + header)
Filter bar (range, entidade, segmento)
KPI row (1–3 heróis + deltas)
Chart band (tendência / comparação)
Detail (tabela, fila, lista acionável)
| Type | Density | Priority |
|---|---|---|
| Admin | Compact, scannable | Freshness + actions |
| BI | More air, storytelling | Comparison + export |
Don't mix a "marketing hero" with an operational table in the same viewport without hierarchy.
Design
- Aligned grid; tabular numbers (
font-variant-numeric: tabular-nums) in KPIs. - Semantic color for status and delta (up/down, success/danger) — not a rainbow per series.
- Typography: dominant KPI value; secondary label and period; readable metric source (definition).
- Avoid "stat soup": many identical cards kill hierarchy.
- Required states: loading (grid skeleton), empty (no data in the filter), error (retry), partial (one series failed).
- Product tokens; no loose hex values. Light/dark: same roles (
better-web-interface). - Admin density: smaller padding, dense tables; BI: more breathing room between bands.
Data and filters
- Explicit contract: time series (
t,value), dimensions, totals,asOf/ timezone. - Filters (range, tenant, product) applied on the server for large datasets; the client only refines the local UI.
- URL or serializable state for shareable filters (panel deep link).
- Timezone and "today" documented; don't mix UTC and local without a rule.
- Refresh: manual + sensible interval; avoid aggressive polling that melts the API and UX.
- Authz: every query/aggregation respects tenant/role — the dashboard is not a permission bypass.
React (focus)
type Kpi = { label: string; value: number; deltaPct?: number; asOf: string };
function KpiCard({ label, value, deltaPct, asOf }: Kpi) {
return (
<article aria-label={label}>
<p>{label}</p>
<p>{formatMetric(value)}</p>
{deltaPct != null ? <p data-delta={Math.sign(deltaPct)}>{formatDelta(deltaPct)}</p> : null}
<p>As of {asOf}</p>
</article>
);
}
- Page: Server Component fetches aggregates when the stack allows; Client islands for interactive filters and charts.
- Widgets presentational pure; data fetching at the edge (page/hook/loader) — do not hide fetch inside each card.
- Compose:
DashboardShell→FilterBar→KpiRow→ChartPanel→DataTable. - Chart lib: the project's (Recharts, Visx, ECharts, etc.); wrap it in a wrapper (
<TrendChart />) to swap it later. useMemo/memoonly with evidence (expensive re-render in chart/table).- Large tables: virtualize; pagination/cursor on the server.
- Avoid prop-drilling filters: lightweight context or state on the page + explicit props.
Charts
| Question | Type |
|---|---|
| How has it evolved over time? | Line |
| Who/what is being compared? | Bar |
| Accumulated volume? | Area (with care) |
| Part of the whole (few slices)? | Pie/donut only if ≤5 categories |
| Distribution / outliers? | Histogram / box (if the audience understands it) |
- Readable axes; tooltip with value + period; legend does not rely only on color.
- Maximum ~3–5 series on the same axis; facet instead of spaghetti.
- No 3D, no long animations; respect
prefers-reduced-motion. - A11y: chart title/description; alternative data table when the value is critical.
- Drill-down: click on the point/bar → same page with filter or detail route.
Admin vs BI
| Admin | BI | |
|---|---|---|
| Goal | Operate / unblock | Understand / decide |
| Freshness | Seconds–minutes | Hours–days ok |
| CTA | Links/actions in the row | Export, save view |
| Detail | Queues, errors, users | Series, cohorts, breakdown |
- Destructive actions (ban, refund, delete): confirmation + authz on the server — the button in the dashboard is not the security.
- Admin: highlight anomalies (error, breached SLA) above vanity metrics.
Anti-patterns
- 12 KPIs with no heroes and no order
- Decorative charts that do not answer a question
- Auto-refresh every 1s on a historical panel
- Filtering millions of rows only on the client
- “Generic AI” dashboard: glass cards, purple gradient, rainbow series, badges everywhere
- Mixing 6 time granularities in the same view without control
- KPI with no definition / no
asOf - Hiding empty/error behind a zeroed chart
Implementation flow
- Write the question and the 1–3 hero metrics.
- Define filters + data contract (+ authz).
- Build the anatomy (shell → KPI → chart → detail).
- Implement reusable React widgets + the project's chart wrapper.
- Cover loading/empty/error; test filter deep link.
- Measure: payload, time to useful KPI, table interaction.
Completion criteria
- Clear question and audience
- KPI → trend → visible detail hierarchy
- Correct filters (server-side when necessary) and shareable
- loading/empty/error states covered
- Charts suited to the question; limited palette
- Authz respected in queries
- Acceptable performance at real volume (without freezing the UI)
- Consistent with the product's design system