Skip to content
microcharts
ReferenceFrontierEtaBar

EtaBar

React ETA bar chart — remaining time sized by the observed rate, not linear interpolation. Tiny accessible SVG microchart; RSC-safe static or /interactive.

EtaBar answers "how long is this actually going to take, given how it has actually been going?". Its x-axis is time, not fraction: the solid part is the elapsed share of the predicted total, and the muted remainder is sized by the observed rate. When the rate drops, the remainder honestly grows — the download bar, told truthfully.

EtaBar
interactive · 3.55 kB · static · 2.18 kB

Install

Import & usereact/eta-bar
import { EtaBar } from "@microcharts/react/eta-bar";<EtaBar progress={0.64} elapsed={3.6} rate={0.18} etaFormat={(t) => `${Math.round(t)} min`} title="Export" />
Needs package + stylesheetSet up with AI

Try it

When to use it

  • Good for — download / export progress, job-queue ETAs, deploy timers.
  • Avoid for — fraction-only progress (Progress) or unbounded counters (Delta).

Sizing

Motion, and reduced motion

The interactive entry eases the elapsed/remainder split (SVG x/width) to its new geometry whenever progress, elapsed, or rate change — the split only ever moves to a value computed from data you passed in on that render; it never interpolates ahead of an as-yet-unseen future frame. The transition lives in shared CSS (.mc-eta-live rect), so it inherits the shared :where(.mc-root *) { transition: none } reduced-motion block: with prefers-reduced-motion: reduce the bar snaps straight to each new split instead of easing. The live region re-announces the forecast on a throttle (announceEvery, default 10 s) so a fast-updating value doesn't spam a screen reader.

Variants

percent
locale

Without an etaFormat, the remaining-time figure falls back to format/locale — under de-DE that's a decimal comma ("11,6") rather than the English decimal point ("11.6"). Pass etaFormat (as the hero example does) to add a unit; it always receives the raw number, so it can format the unit itself in whatever locale-aware way the caller needs.

Edge cases

rate hits zero (stalled)
remainder far exceeds elapsed (overflow)
done

An explicit rate of 0, or an absent rate from which no positive average can be derived from elapsed, renders the remainder as a diagonal-hatched texture instead of a solid bar — "unknown," not a hidden guess — and the summary says "stalled" rather than naming a duration. When the predicted remainder is far larger than the elapsed time, the done segment would round to an unreadable sliver, so the geometry clamps it to a visible 10% and adds a chevron marking the clamp as approximate. At progress >= 1 the bar fills completely regardless of rate.

Four homes

Why this default

The ETA label is the default because "how long?" is the question a plain progress bar pretends to answer. The remainder is sized by the observed rate, never linear interpolation — so a stalling transfer looks stalled instead of marching to a fake finish. When the rate is unknown or zero the remainder becomes an indeterminate texture and the summary says so, rather than inventing a countdown.

Accessibility

The accessible name is the honest forecast — "42% done; about 11.6 remaining at the current rate." A stalled transfer reads "30% done; stalled." and a finished one "Done." The interactive entry re-announces on a throttle as the forecast changes.

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
progress*numberCompleted fraction 0–1.
elapsed*numberTime spent, any unit.
ratenumberProgress per time unit — pass a recent-window rate.
label"eta" | "percent" | "none"The remaining-time read is the product.
etaFormat(t: number) => stringUnit-bearing ETA label ("2 min") — the caller owns units.
announceEverynumberinteractiveMinimum ms between live-region announcements as the ETA streams (default 10000).
animatebooleaninteractiveOpt-in entrance motion when the chart mounts client-side — add import "@microcharts/react/motion" once. Inert on the server, on hydrated server HTML, and under prefers-reduced-motion.

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