DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

Progress

Communicate how far an operation has come — linear bar or circular ring, five semantic intents, five sizes, name/value/description anatomy, custom ranges with formatted announcements, segmented onboarding strips, on-color tone, indeterminate animation with reduced-motion substitutes, and LTR/RTL support.

Playground

Installation

pnpm add @tessinaui/ui

Usage

import { Progress } from "@tessinaui/ui";
{/* Determinate */}
<Progress value={65} aria-label="Upload" />

{/* Name + value header */}
<Progress value={65} label="Backing up" showLabel showValue />

{/* Custom range, announced as "64 of 250" (aria-valuetext) */}
<Progress value={64} max={250} showValue formatLabel={(v, m) => `${v} of ${m}`} label="Films" />

{/* Indeterminate — the label stays visible */}
<Progress indeterminate label="Contacting server…" showLabel />

{/* Circular with the value in the center */}
<Progress type="circular" value={65} size="lg" showValue aria-label="Sync" />

{/* Segmented onboarding strip — still ONE progressbar */}
<Progress segments={4} value={50} aria-label="Step 2 of 4" />

When to use

  • A task with measurable progress — uploads, downloads, imports, multi-step processing. Prefer determinate whenever the work is measurable (HIG), and switch indeterminate → determinate the moment it becomes so.
  • Longer than ~5 seconds — shorter waits take a Spinner; loading content of known shape takes a Skeleton (M3/Carbon thresholds).
  • Keep one bar per operation — combine steps into a single monotonic fill and change the description per step; a bar that rewinds destroys trust (Fluent/Carbon).

When not to use

  • Point-in-time gauges — storage used, quota left, battery, scores. That is Meter (role="meter"), including stacked parts-of-a-whole via MeterGroup. A progressbar announces task completion; a meter states a measurement.
  • Labeled step journeys — numbered checkout/wizard steps with captions belong to Stepper. segments here is purely visual track division.
  • Sub-second actions — show nothing under ~200ms.
  • Forms don't automatically need one: GOV.UK ships question flows with a plain "Question 3 of 9" caption after finding step indicators unnoticed — test without one first.

Examples

Default

A basic linear progress bar showing a determinate value.

Types

Linear bar or circular ring via the type prop.

Intents

Five semantic intents — none (neutral primary), info, success, warning, and error.

Sizes

Five sizes — xs hairline through xl.

Labels and values

label names the operation (start-aligned); showValue renders the formatted value end-aligned in the header, or inside the fill on lg/xl.

Custom range and formatting

max re-bounds the range; formatLabel shapes both the visible value and the announced aria-valuetext.

Description and outcome states

A helper line under the bar carries status detail; intent="success" confirms completion, intent="error" + description explains failure and the way forward.

Segments

Split the track into discrete cells for onboarding strips — one accessible progressbar throughout.

On color

tone="on-color" derives track and fill from currentColor for coloured plates; intent colors intentionally give way to the surface ink.

Circular center content

The ring accepts children as its center — counts, icons, or a check on completion.

Indeterminate

Animated unknown-duration state for both types. Under prefers-reduced-motion the slide/spin is replaced by an opacity pulse — activity stays signalled without translation or rotation.

Upload list

Composition: per-file rows with waiting/uploading/paused/error/complete states, pause/cancel/retry controls, and a polite live region carrying the announcements a progressbar itself never makes.

Loading

Skeleton placeholders shown before a value is known — same box as the real component (§2b), label bar sized in characters.

API Reference

Progress

PropTypeDefaultDescription
type"linear" | "circular""linear"Bar or ring
valuenumber0Current progress, clamped to [0, max]; non-finite input renders empty
maxnumber100Upper bound; max <= 0 falls back to 100
intent"none" | "info" | "success" | "warning" | "error""none"Semantic fill color; none renders the neutral primary fill
tone"default" | "on-color""default"On coloured plates: monochrome currentColor track/fill
size"xs" | "sm" | "md" | "lg" | "xl""md"Track height (linear) / ring diameter (circular)
rounded"none" | "sm" | "md" | "lg" | "full""full"Corner rounding of track, fill and segments (linear only)
labelstring—Accessible NAME of the operation; becomes aria-label
showLabelbooleanfalseRender label visibly — above the bar / below the ring
showValuebooleanfalseRender the formatted value — header end / ring center
valuePosition"end" | "inside""end"Linear value placement; inside needs lg/xl
formatLabel(value, max) => stringrounded %Formats the visible value AND aria-valuetext
descriptionReactNode—Helper line under the bar / ring — status text, error detail
segmentsnumber—Split the linear track into N cells; ignored while indeterminate
indeterminatebooleanfalseUnknown-duration animation — value ignored, value labels suppressed
childrenReactNode—Circular only: custom center content
dir"ltr" | "rtl""ltr"RTL mirrors the linear fill; the ring never mirrors (spec)
classNamestring—Additional classes on the root wrapper
variant——Deprecated — use intent (default → none)
labelPosition——Deprecated — use showValue + valuePosition; while present, the old value-display behaviour is kept

ProgressSkeleton

PropTypeDefaultDescription
type / size / roundedas Progress—Same geometry sources as the real component (§2b)
showLabelbooleanfalseReserve the header / under-ring label line
labelCharsnumber8Characters of the real label — width-true text bar
labelstring"Loading"What AT announces while the placeholder is up — rendered as sr-only text, not aria-label

Sizes

SizeLinear heightCircular diameter
xs4px (h-1)24px
sm6px (h-1.5)32px
md10px (h-2.5)48px
lg16px (h-4)64px
xl32px (h-8)128px

Ring strokes are viewBox-proportional — they scale with the diameter.

DOM contract

Stable hooks for styling, tests and agents:

AttributeWhereValues
data-slotevery partprogress, progress-header, progress-label, progress-value, progress-track, progress-ring, progress-fill, progress-segment, progress-description, progress-skeleton
data-statewrapper + controlloading · complete (value ≥ max) · indeterminate
data-value / data-maxwrapper + controlcurrent numbers; data-value absent while indeterminate

Accessibility

Pattern: ARIA progressbar role. Not an interactive widget — no keyboard interaction is defined.

  • The role element carries aria-valuenow / aria-valuemin / aria-valuemax; aria-valuenow is omitted entirely while indeterminate (never zeroed), and the fallback name becomes "Loading", never a phantom "0%".

  • Name: pass label (or aria-label/aria-labelledby — forwarded aria-* reaches the role element and outranks the fallback). AT announces the value as a percent of range; formatLabel overrides that via aria-valuetext when percent would mislead ("64 of 250").

  • A progressbar is not a live region (WCAG ARIA25): value updates are not announced by themselves. Pair it with a polite role="status" element for spoken updates, and put aria-busy="true" + aria-describedby on the region being loaded — the upload-list example shows the wiring.

  • The wrapper/control split routes consumer aria-* to the role element and data-*/test ids to the role-less wrapper — both types, verified by DOM-read tests.

  • Reduced motion: indeterminate translation/rotation swaps to an opacity pulse; value transitions are disabled. Verified per engine with computed styles, not class presence.

  • Contrast (measured, both themes): fills pass 3:1 against track and page (WCAG 1.4.11) — except intent="warning" in the light theme (2.17:1 vs page), the system-wide known light-warning token limitation; pair a warning bar with its description rather than relying on hue. The on-color tone derives from the surface ink contract (§3).

  • Skeleton announcement: ProgressSkeleton is a role="status" region whose text is sr-only, never an aria-label — a live region announces its CONTENT, so a label-only region is silent on insertion (VoiceOver reported the earlier version as "empty application status").

AT matrix: VoiceOver + Safari verified locally. NVDA and JAWS — UNTESTED (no Windows host; see QA §5.6 reality note). Status stays beta until an NVDA pass is recorded.

Notes

  • Near-zero values: a rounded fill keeps a minimum started-dot width (one track-height) as soon as value > 0, so 0.5% reads as "started", not as a squashed sliver — and 0 renders an empty track.
  • Inside value: only on size="lg" | "xl" and never under tone="on-color" — it falls back to the header end position.
  • Ring center: content is capped to the square that fits inside the arc (inner ⌀ / √2 — 27px at md, 35px at lg, 73px at xl) and clipped beyond it, so a long value can never paint over the stroke. Keep it to about four characters: a percentage, a count, an icon. Longer text belongs in label or description below the ring — showValue with a formatLabel fraction reads as "26%" inside and "64 of 250 films" underneath.
  • Small rings: xs (24px) and sm (32px) leave 14–19px of usable square, which cannot hold legible type, so showValue renders the value under the ring at those sizes instead of shrinking it. Custom children still render inside — that is where a check or a dot belongs.
  • Meter boundary: quota/storage/battery displays are measurements, not progress — use Meter / MeterGroup (role="meter").
PopoverPromptInput

On this page

PlaygroundInstallationUsageWhen to useWhen not to useExamplesDefaultTypesIntentsSizesLabels and valuesCustom range and formattingDescription and outcome statesSegmentsOn colorCircular center contentIndeterminateUpload listLoadingAPI ReferenceProgressProgressSkeletonSizesDOM contractAccessibilityNotes