Pulse dashboards
NewOne analytics app in four looks: a store’s pixel-column sales, a content team’s stacked revenue, a sales team’s area chart and funnel, a studio’s spiky billing lines.
pulse-dashboards/pulse-dashboards.tsxWelcome back, Mara!
Total revenue
$284,920
up8.2%vs prior 30 days
Orders
1,842
up4.1%vs prior 30 days
Average order
$154.60
down1.3%vs prior 30 days
Returning
38.4%
up2.6%vs prior 30 days
Sales
up1.5%vs last month
$413K
Total sales
New userExisting
Recent orders
- Ines Park3 items$214.50Paid
- Theo Lindqvist1 item$89.00Paid
- Amara Osei2 items$156.20Pending
- Jonah Reyes5 items$402.75Paid
- Lena Brandt1 item$64.00Refunded
- Kofi Mensah2 items$131.40Paid
- Rosa Alvarez4 items$288.00Paid
Traffic sources
12 months
- Organic search4.1K
- Direct2.9K
- Social1.8K
- Referral1.2K
- Email760
Category mix
Last 30 days
Sales by hour
$4.8K
Peak · Sat 9p–10p
Top products
Units
- Linen overshirt$26.8K412
- Stoneware mug set$13.6K388
- Wool throw$19.3K214
- Desk lamp, brass$15K167
The live component, numbered part by part. Every measurement is read from it as it is drawn. Point at a row to outline its part, or at a part to find its row.
When to use
- The home screen of an analytics product: a sidebar of sections, a row of KPIs with deltas and the one chart people come for.
- A family of products that should feel related but not identical: one set of parts, a look per product through
variant. - Charts people read by scrubbing: a tooltip under the pointer or the arrow keys, and a range toggle that animates the data rather than reloading it.
- A workspace app where switching workspaces, searching the nav and opening the account menu belong in the same shell as the figures.
When not to use
- One number or one chart on a page. Use a stat tile or a chart card on its own; the window and sidebar are dead weight.
- Acting on rows: repaying, approving, editing. Use a dashboard built around a table and its actions, like the loans dashboard.
- Live, second-by-second monitoring. The charts animate on a range change, not on a stream; use a log or a live chart that appends.
- Inside a page that already has its own navigation. The sidebar would be a second nav; take the charts out and use them in cards.
- Exploring data freely (pivots, filters, custom dimensions). Use a query builder or a notebook; these charts show fixed series.
You can add this component with the shadcn CLI or by hand:
Into components/pulse-dashboards of any React project, with no setup:
npx shadcn@latest add https://brunodelorence.com/r/pulse-dashboards.json- Copy the component files
Copy these files into one folder of your project, keeping their relative imports together, or download them as a ZIP:
- Import it
As shown under Usage, below.
Either way it needs React 19.2 and a bundler that supports CSS Modules, such as Next.js or Vite.
Usage
import { PulseDashboards } from './pulse-dashboards';export function AnalyticsPage() { return ( <PulseDashboards variant="crm-dark" switcher={false} workspace="Halden Co." user={{ name: 'Mara Ellison', email: 'mara@halden.co' }} autoplay={false} /> );}Each state below is a live instance, locked so you can study it.
The pieces it is built from, each with its measurements read live.
Four dashboards of one family that share their parts: the same sidebar with a workspace switcher, search and sections, KPI cards with deltas, and SVG charts with tooltips. Commerce stacks its sales in small pixel cells over 14 days, a month or three; Content stacks today’s revenue over yesterday’s, hour by hour; CRM draws revenue as an area with a date pill, retention as thin grouped bars and its leads as a funnel; Overview draws revenue, net revenue and conversion as spiky lines. A switcher above the window moves between them; two are dark and two light, and the colorScheme prop flips any of them.
- One component, a variant prop. The four looks share the window, the sidebar (its sections, help links and account come from a table per look), the KPI cards, the range toggles, the tooltip and the charts; each look sets a few tokens (sidebar, panel, trays, cards) and sizes (a 253 px sidebar with 40 px rows for Commerce, 248 px with 32 px rows for CRM and Overview) on data-variant.
- Every chart is an SVG drawn in CSS pixels at the width a ResizeObserver reads from its slot, so cells, dashes and strokes stay crisp at any size; the axis labels are HTML beside it, and a narrow chart labels fewer columns rather than letting them collide.
- The pixel columns stack cells of a fixed 9 px pitch, each worth the axis top over the rows that fit: the bright cells are existing customers, the grey ones new. A range change remounts the columns and every cell pops in by transform and opacity, staggered by column and row.
- The stacked bars are full-height rects scaled by scaleY from the bottom and moved up by translateY, so a range change glides with a CSS transition. The lines (CRM’s area, Overview’s spikes) keep thirty points in every range, and their values tween over half a second with requestAnimationFrame, rebuilding the path only while that lasts.
- Hovering a chart picks the nearest column, point, cell or stage and shows a tooltip on the side with more room; CRM also slides a date pill along its axis. Each chart is one tab stop: the arrows move along it, Home and End jump, Escape lets go, and a polite live region reads the tooltip.
- The nav works: a section opens its own (empty) page, the search field (or ⌘K) filters the nav and Enter opens the first match, the workspace and account menus open and close on Escape or a press elsewhere, CRM’s tasks tick off and the panel button folds the sidebar to an icon rail.
- Layout from the component’s own width and the main column’s (container queries): four KPI columns down to 860 px of main column, then two, then one; under 720 px the sidebar is a drawer behind a menu button, the window grows with its content and the cards stack.
- While on screen and untouched, a demo pointer scrubs the look’s main chart, switches its range there and back, looks at a lower chart, visits a section and moves on to the next look. A press, a key, the wheel or a mouse over it hands it to the reader; it comes back after 9 s of quiet, and never under reduced motion.
- Colours are light-dark() tokens on the root; each look sets its own scheme (data-color-scheme), and colorScheme overrides it, so a dark look reads as a light one and back.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'commerce-dark' | 'content-light' | 'crm-dark' | 'overview-light' | 'commerce-dark' | The look: a store’s sales, a content team’s revenue, a sales team’s pipeline or a studio’s billing. |
switcher | boolean | true | Shows the row of looks above the window. |
onVariantChange | (variant: Variant) => void | — | Called when the switcher picks another look. |
workspace | string | 'Halden Co.' | The workspace’s name in the switcher. |
user | { name: string; email: string } | { name: 'Mara Ellison', email: 'mara@halden.co' } | Who is signed in: the foot of the sidebar (with initials for an avatar) and the greeting. |
defaultState | PulseDashboardsState | — | Opens in this moment, held still: page (a section’s id), range (the main chart’s), point (its active column or point, with the tooltip), menu (workspace or account), drawer (the sidebar open on a narrow screen), collapsed (the sidebar as an icon rail) and query (words in the nav’s search). Turns the demo off. |
autoplay | boolean | true | While on screen and untouched, a demo pointer shows the main interactions and moves through the looks. Any real input stops it. |
colorScheme | 'light' | 'dark' | 'system' | — | Light or dark colours, or the reader’s; without it, the look’s own scheme. |
className | string | — | Extra class for the root. |
The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';/* * Pulse dashboards: one analytics app in four looks that share their parts. An app window with a * sidebar (workspace switcher, search, sections, help links, the signed-in person) and a main * column of KPI cards and charts: * * - commerce-dark: a store's sales. KPI cards with small pixel columns, sales as stacked pixel * columns (new and existing customers) over 14 days, a month or three months, traffic sources as * dotted bars, the category mix, sales by hour of the week and the top products. * - content-light: a content team. KPI cards in a header tray, gross revenue by hour as stacked * bars (now over before) for a day, a week or a month, channels, the next posts and stats. * - crm-dark: a sales team. KPI cards with badges, revenue over time as an area with a tooltip and * a date pill, retention by segment as thin grouped bars, and the leads funnel. * - overview-light: a studio's billing. Spiky lines for revenue, net revenue and conversion, the * recent clients and the upcoming renewals. * * Every chart is SVG with a tooltip on hover or keyboard focus (charts.tsx); the range toggles * animate the data. The nav works: another section opens its (empty) page, search filters the * nav, the workspace and account menus open. Below 720 px of its own width the sidebar becomes a * drawer behind a menu button and the cards stack (container queries). */* Pulse dashboards (pulse-dashboards.tsx): one analytics app in four looks. Greys only, with a green for gains and a red for losses: near-black windows with cards a step lighter by night, an off-white sidebar and white cards by day. Each look tunes a few tokens (sidebar, panel, trays, cards) and a few sizes (sidebar width, row height, paddings) on [data-variant]. Colours are the tokens below, light-dark(light, dark): the component takes the scheme its look has (data-color-scheme), or the one its colorScheme prop sets. Sizes are the app's own at 1440 px: a 248–253 px sidebar with 32–40 px rows, 14 px text, KPI values at 24 px. Layout comes from the component's width and the main column's (container queries): four KPI columns on a wide main column, two on a narrow one, one on a phone; the sidebar a drawer under 720 px. Motion. Pixel cells pop in by transform and opacity, bars grow by scaleY, the dotted bars fill by clip-path, menus and the drawer come in by transform and opacity. Nothing moves under reduced motion. */.root { --stage-ink: #0a0a0a; --switch: #f2f2f2; --switch-on: #fff; --switch-edge: #e3e3e3; --window-edge: #e2e2e2; --sidebar: #fafafa;// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';/* * The charts. Each is an SVG drawn in CSS pixels at the width its slot measures (a ResizeObserver), * so strokes, cells and dashes stay crisp at any size; text labels are HTML beside or under it. * * - Every chart scrubs: the pointer's x (or y) picks the nearest column, point or stage, and a * tooltip shows its figures. The chart itself is one tab stop: the arrow keys move along it, * Home and End jump, Escape lets go, and a polite live region reads the tooltip out. * - The active item is held by the parent (`active`, `onActive`) so the demo can point at it too. * - Range changes animate by transform and opacity only: pixel columns pop in cell by cell, * stacked bars grow by scaleY, and lines morph point by point (their values tween over half a * second; the path is rebuilt each frame only while that lasts). Under reduced motion they jump. */import { useEffect, useId, useLayoutEffect, useRef, useState, type CSSProperties,// Copyright (c) 2026 Bruno Delorence. MIT License./* * The figures of the four screens: a store's sales, a content team's revenue and posts, a sales * team's pipeline and a studio's billing overview. All invented. The ranges a chart switches * between are written out for the range the screen opens on and derived for the others by a seeded * random walk, so they are the same on every load (and on the server and the client). */export type Variant = 'commerce-dark' | 'content-light' | 'crm-dark' | 'overview-light';export const VARIANTS: { id: Variant; label: string; scheme: 'light' | 'dark' }[] = [ { id: 'commerce-dark', label: 'Commerce', scheme: 'dark' }, { id: 'content-light', label: 'Content', scheme: 'light' }, { id: 'crm-dark', label: 'CRM', scheme: 'dark' }, { id: 'overview-light', label: 'Overview', scheme: 'light' },];/** A small seeded generator (mulberry32): the same numbers on every load. */export function seeded(seed: number) { let state = seed >>> 0; return () => { state = (state + 0x6d2b79f5) >>> 0; let t = state;// Copyright (c) 2026 Bruno Delorence. MIT License./* * The icons: thin line drawings on a 24-unit grid in currentColor, sized by the stylesheet. All are * decorative (aria-hidden): the text beside each one names what it does. */import type { ReactNode } from 'react';const PATHS = { dashboard: ( <> <rect x="3.5" y="3.5" width="7" height="7" rx="1.6" /> <rect x="13.5" y="3.5" width="7" height="7" rx="1.6" /> <rect x="3.5" y="13.5" width="7" height="7" rx="1.6" /> <rect x="13.5" y="13.5" width="7" height="7" rx="1.6" /> </> ), calendar: ( <> <rect x="3.5" y="5" width="17" height="15.5" rx="2" /> <path d="M8 3v4M16 3v4M3.5 10h17" /> <path d="M8 13.5h.01M12 13.5h.01M16 13.5h.01M8 17h.01M12 17h.01M16 17h.01" strokeWidth="2" /> </>// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import type { CSSProperties, Ref } from 'react';/* * The demo's pointer, and the steps that move it. A demo is a script of awaited steps: wait, or * glide the pointer somewhere. Stopping the demo (the reader takes over, or it scrolls away) ends * the step in progress at once: its promise rejects, which ends the script right there, and the * pointer fades out. * * Each glide is a curved flight played by the Web Animations API, so nothing runs frame by frame. * The points of the flight are handed to whoever wants to move along with it. The arrow and the hand are HTML boxes * that swap by their visibility: a change of style inside an SVG would lay the SVG out again. */export type Point = { x: number; y: number };/** A glide's path and timing, for an animation that plays along with it. */export type Flight = { points: Point[]; timing: KeyframeAnimationOptions };export type Demo = { /** Resolves after `ms`. */// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useCallback, useEffect, useRef, useState } from 'react';/* * When a demo may play on its own: while part of it is on screen and the tab is visible, never * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it * takes back over `idle` ms after their last input. */export function useAutoplay<T extends HTMLElement>(idle = 6000) { const ref = useRef<T>(null); const [visible, setVisible] = useState(false); const [reduced, setReduced] = useState(true); const [taken, setTaken] = useState(false); const timer = useRef(0); useEffect(() => { const element = ref.current; if (!element) return; let onScreen = false; const update = () => setVisible(onScreen && !document.hidden); const observer = new IntersectionObserver(- Landmarks and names: the sidebar is a complementary region holding a navigation with aria-current on the open section, the dashboard is a main with a heading, and each card is a section named after its figure.
- The looks’ switcher and every range toggle are radio groups with one tab stop (arrows move and pick). The workspace and account buttons open menus (menuitemradio for the workspaces) that close on Escape or a press elsewhere; the search field filters the nav and Enter opens the first match.
- Each chart is an image with a summary for its name and one tab stop: the arrows, Home and End move along it and a polite live region reads the active column, point or stage. Deltas say up or down in words, not only by colour and arrow.
- Section changes, workspace changes and look changes are announced in a polite live region. On a phone the menu button opens the sidebar as a drawer (aria-expanded, aria-controls) with its own close button.
- Under reduced motion nothing pops, grows or tweens and the demo never plays; the demo pointer is hidden from assistive technology.