Skip to content
microcharts
ReferenceExpressiveBreathingDot

BreathingDot

React breathing dot chart — ambient load as pulse rate, with a static reduced-motion twin. Tiny accessible SVG microchart; RSC-safe static or /interactive.

BreathingDot answers "how loaded is the system right now?" as an ambient signal. In the interactive entry the dot pulses — slow and small when calm, fast and large when strained — so you feel the state without reading a number. The static frame is a real chart with zero JavaScript: a core dot colored by band, and a ring whose distance from the core is the level. This is a low-precision ambient read; for an exact figure reach for Progress or Sparkline.

BreathingDot
interactive · 3.17 kB · static · 1.77 kB

Install

Import & usereact/breathing-dot
import { BreathingDot } from "@microcharts/react/breathing-dot";<BreathingDot value={0.42} title="Load" />
Needs package + stylesheetSet up with AI

Try it

Motion, and reduced motion

The pulse is the encoding — its rate and amplitude are snapped to three named bands (calm, elevated, strained), so the motion is readable, not decorative. This is the deliberate exception to the no-looping-animation rule, allowed only because the loop parameter (the rate) is the datum. The animation is gated on two things: it pauses when the dot scrolls off-screen (a shared observer), and it never runs at all under prefers-reduced-motion — reduced-motion readers get exactly the static frame, where the ring's distance from the core already carries the level. The band is announced through a polite live region only when it changes, never on every pulse.

When to use it

  • Good for — an ambient "how strained is it right now" read, a live status dot in a header or KPI card, or per-node load in a dense table.
  • Avoid for — an exact load figure (Progress, Sparkline), discrete events (HeartbeatBlip), or a trend over time (Sparkline).

Sizing

One size prop, in viewBox units, sets the glyph box — it defaults to 16, about the height of a line of body text. There is no separate width/height: the dot is square, and the rendered box widens only by the gutter a label reserves. fontSize follows size unless you set it.

Variants

bands + the exact-read escape hatch

Edge cases

unknown — gray, no ring, never pulses
out-of-range clamps to the calm/strained edge
NaN reads as unknown, same as null
locale

format/locale reach the label="value" percent numeral — always a whole percent, so a locale mostly changes digit shapes here, not grouping or decimals. The band words in the accessible summary ("calm" / "elevated" / "strained") stay in strings (BreathingDotStrings), not the number formatter.

Four homes

Why this default

Three bands, because ambient motion supports about three discriminable states — more would be false precision. The band color is always doubled: by the ring offset in the static frame and by the pulse rate in motion, so the state never rests on color alone. When the value is unknown (null or NaN) the dot goes gray, drops its ring, and never pulses — an unknown system must not look calm. The boundary with HeartbeatBlip: a continuous level is a BreathingDot; discrete arriving events are a HeartbeatBlip.

Accessibility

The accessible name is the level and its band word — "Load 20% — calm." — or "Load unknown." when the value is missing. The band color is never the only signal (ring offset and pulse rate double it), motion is fully gated on prefers-reduced-motion, and the live region announces band changes only.

This chart is a single unit, so there is nothing to rove between: a click, tap, Enter or Space selects it and fires onSelect, and no selection stays pinned. That is the scalar half of the shared interaction contract.

Props

PropTypeNotes
value*numberLevel 0–1 (clamped). null / NaN → unknown.
thresholds[number, number]calm / elevated / strained edges (default [0.5, 0.8]).
label"value" | "none"Percent numeral beside the dot.

Plus the shared grammar — data, domain, color, title, summary, format — and the layout props (width, height, className, style) that every chart accepts. Interactive entries also share animate and live, and — wherever a chart has more than one navigable unit — onActive, onSelect, selectedIndex and defaultSelectedIndex; and — wherever the chart shows a hover value — readout. See the shared grammar.

Related charts