---
title: "Progress indicator"
description: "Shows how far a task has come, or that it is under way when how far is unknown, as a bar or a ring."
url: "https://tanrei.chnotchy.com/components/progress-indicator/"
last_updated: 2026-10-23T21:17:12.000Z
---

# Progress indicator

> Shows how far a task has come, or that it is under way when how far is unknown, as a bar or a ring.

## Overview

### Usage

The Web and React samples show the current spec. SwiftUI, Compose, Flutter and React Native are lagging: they do not have `separation` yet, and their samples show what they ship.

**Web**

```html
<tanrei-progress-indicator value="0.4" label="Uploading report.pdf"></tanrei-progress-indicator>
<tanrei-progress-indicator value="3" max="10" value-text="3 of 10 files" label="Uploading"></tanrei-progress-indicator>

<!-- No value: indeterminate, as a <progress> without one. -->
<tanrei-progress-indicator variant="circular" size="small" label="Syncing"></tanrei-progress-indicator>

<!-- A gap between the indicator and the rest of the track, whatever the design system chose. -->
<tanrei-progress-indicator separation="gap" value="0.4" label="Uploading report.pdf"></tanrei-progress-indicator>

<script type="module">
  import '@tanrei/web/components/progress-indicator';
  const bar = document.querySelector('tanrei-progress-indicator');
  bar.value = sent / total;
</script>
```

**React**

```tsx
import { ProgressIndicator } from '@tanrei/react';

<ProgressIndicator value={sent / total} label="Uploading report.pdf" />
<ProgressIndicator value={files} max={10} valueText={`${files} of 10 files`} label="Uploading" />
<ProgressIndicator variant="circular" size="small" value={null} label="Syncing" />
<ProgressIndicator separation="gap" value={sent / total} label="Uploading report.pdf" />
```

**SwiftUI**

```swift
import Tanrei

TanreiProgressIndicator("Uploading report.pdf", value: sent / total)
TanreiProgressIndicator("Uploading", value: Double(files), in: 0...10, currentValueLabel: "\(files) of 10 files")

// No value (the default) is indeterminate.
TanreiProgressIndicator("Syncing", variant: .circular, size: .small)
```

**Compose**

```kotlin
import io.github.chnotchy.tanrei.components.TanreiProgressIndicator
import io.github.chnotchy.tanrei.components.TanreiProgressIndicatorVariant
import io.github.chnotchy.tanrei.tokens.TanreiProgressIndicatorSize

// progress is a lambda: a new value redraws without recomposing.
TanreiProgressIndicator(progress = { sent / total }, contentDescription = "Uploading report.pdf")
TanreiProgressIndicator(
    progress = { files.toFloat() },
    contentDescription = "Uploading",
    valueRange = 0f..10f,
    stateDescription = "$files of 10 files",
)

// The overload without progress is indeterminate.
TanreiProgressIndicator(
    contentDescription = "Syncing",
    variant = TanreiProgressIndicatorVariant.Circular,
    size = TanreiProgressIndicatorSize.Small,
)
```

**Flutter**

```dart
import 'package:tanrei/tanrei.dart';

TanreiProgressIndicator(value: sent / total, semanticsLabel: 'Uploading report.pdf');
TanreiProgressIndicator(
  value: files.toDouble(),
  max: 10,
  semanticsValue: '$files of 10 files',
  semanticsLabel: 'Uploading',
);

// value: null (the default) is indeterminate.
const TanreiProgressIndicator(
  variant: TanreiProgressIndicatorVariant.circular,
  size: TanreiProgressIndicatorSize.small,
  semanticsLabel: 'Syncing',
);
```

**React Native**

```tsx
import { ProgressIndicator } from '@tanrei/react-native';

<ProgressIndicator accessibilityLabel="Uploading report.pdf" value={sent / total} />
<ProgressIndicator accessibilityLabel="Uploading" value={files} max={10} valueText={`${files} of 10 files`} />
<ProgressIndicator accessibilityLabel="Syncing" variant="circular" size="small" />
```

## Specs

- Category: Communication
- Level: base
- Listing: default
- Page: https://tanrei.chnotchy.com/components/progress-indicator/
- Spec: https://github.com/chnotchy/tanrei/blob/main/specs/progress-indicator.spec.json

### Availability

| Design system | Has it | Notes |
| --- | --- | --- |
| Tanrei (`tanrei`) | Yes | — |
| Cupertino (Apple HIG-inspired) (`cupertino`) | Yes | — |
| M3 Expressive-like (Material 3 Expressive-inspired) (`m3-expressive-like`) | Yes | — |
| M3-like (Material 3-inspired) (`m3-like`) | Yes | — |

### Platforms

- Web: the element `<tanrei-progress-indicator>`, registered by importing `@tanrei/web` (or only its own module, `@tanrei/web/components/progress-indicator`).
- React: `ProgressIndicator` from `@tanrei/react`.

| Platform | Name | Status |
| --- | --- | --- |
| Web | `tanrei-progress-indicator` | lagging |
| SwiftUI | `TanreiProgressIndicator` | lagging |
| Jetpack Compose | `TanreiProgressIndicator` | lagging |
| Flutter | `TanreiProgressIndicator` | lagging |
| React Native | `ProgressIndicator` | lagging |

Names a platform gives in its own idiom:

| Spec | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
| --- | --- | --- | --- | --- | --- |
| `value` (prop) | `value` | `value` | `progress` | `value` | `value` |
| `min` (prop) | `min` | `bounds` | `valueRange` | `min` | `min` |
| `max` (prop) | `max` | `bounds` | `valueRange` | `max` | `max` |
| `label` (prop) | `label` | `label` | `contentDescription` | `semanticsLabel` | `accessibilityLabel` |
| `valueText` (prop) | `valueText` | `currentValueLabel` | `stateDescription` | `semanticsValue` | `valueText` |

Statuses: `planned` — In the inventory. Nothing shippable exists yet. `lagging` — An implementation of an earlier spec exists; it has not caught up with the current one. `alpha` — Implemented with tests. The API may still change. `beta` — Feature complete and at parity. Only fixes are expected. `stable` — Covered by semantic versioning. `deprecated` — Still shipped; scheduled for removal.

### Anatomy

- `track`: The full length of the bar, or the full circle of the ring: the whole of the task.
- `indicator`: The part of the track drawn in the indicator color: the fraction done (determinate), or a moving segment or arc (indeterminate). It meets the rest of the track, or stands a gap from it (separation).
- `stop-indicator` (optional): A dot at the end of a determinate bar's track that marks where the task ends. Drawn under either separation whenever its size is above 0; 0 draws none.

### Props

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `variant` | `enum`: `linear`, `circular` | `"linear"` | `variant` | Linear is a bar that fills the width its layout gives it, for content and pages; circular is a ring of a fixed size, for tight places and next to text. A structural choice drawn by code, not a token axis: a design system cannot add a form by values. |
| `size` | `enum`: `small`, `medium`, `large` | `"medium"` | `size` | The bar's thickness (2 / 4 / 8) or the ring's box (16 / 24 / 40). A design system may add values. |
| `separation` | `enum`: `auto`, `divider`, `gap` | `"auto"` | `separation` | How a determinate indicator is told from the rest of its track. divider: it is drawn on the track with no space between them. gap: empty space is left between them. Whether a bar marks the track's end with a dot is comp.progress-indicator.stop-indicator.size, not the separation. Auto takes comp.progress-indicator.track.separation, the design system's answer (divider in the default system). |
| `value` | `number` | — | `value` | How far the task has come, between min and max. Null makes the indicator indeterminate. |
| `min` | `number` | `0` | `min` | The value at which nothing is done. |
| `max` | `number` | `1` | `max` | The value at which the task is done. |
| `label` | `string` | `""` | `label` | The accessible name: what is in progress ("Uploading report.pdf"). Required: a bar alone is not a name. |
| `valueText` | `string` | `""` | `value-text` | How assistive technology reads the value when a percentage is not the natural reading ("3 of 10 files"). Empty reads the platform's percentage. |

### States

`enabled`

### Accessibility

- Role: `progressbar`
- Labeling: label is the accessible name. valueText, when given, replaces the platform's percentage as the spoken value. A visible caption next to the indicator is the product's composition; when it exists, use the same words as label.
- Minimum target size: yes

- Not interactive: the indicator takes no input and is never focusable (PRG-10), so the minimum target holds without extending anything.
- The value is read from the boundary between indicator and track: comp.progress-indicator.indicator.color must stay at least 3:1 against comp.progress-indicator.track.color and against the surface behind it, under divider and under gap alike. A faint track may leave the track's end hard to see; the stop indicator, in a system that gives it a size, marks it.
- Indeterminate is announced as busy with no value: on the web a progressbar without aria-valuenow (and aria-busy), on Apple platforms a progress element with no value, on Android the indeterminate range info, in Flutter a progress indicator with a null value.
- Reduced motion replaces movement with an opacity pulse of at most one per second (PRG-7), below the three-per-second flash threshold.

### Behaviour

Requirements in EARS form, each with its stable id.

- **PRG-1** WHILE value is a number THE progress indicator SHALL be determinate and draw the indicator over the fraction (value − min) / (max − min) of the track, from the track's start: the inline-start of a bar, the top of a ring, turning clockwise.
- **PRG-2** WHILE value is null THE progress indicator SHALL be indeterminate: a bar SHALL move a segment of the indicator, two fifths of the track long, from beyond the track's start to beyond its end once per comp.progress-indicator.motion.cycle-duration with comp.progress-indicator.motion.cycle-easing; a ring SHALL turn an arc of the indicator a quarter of the circle long clockwise once per comp.progress-indicator.motion.cycle-duration at constant speed (sys.motion.easing.linear).
- **PRG-3** THE progress indicator SHALL draw a bar comp.progress-indicator.linear.thickness.medium high for medium (and so on for each size) across the full inline size its layout gives it, and a ring whose outer edge fills a comp.progress-indicator.circular.size.medium square with a stroke of comp.progress-indicator.circular.thickness.medium for medium (and so on for each size).
- **PRG-4** THE progress indicator SHALL draw the track in comp.progress-indicator.track.color and the indicator over it in comp.progress-indicator.indicator.color; the ends of a bar's track and indicator SHALL be rounded by comp.progress-indicator.track.shape, clamped to half the thickness, and a ring's arcs SHALL have round caps unless comp.progress-indicator.track.shape is sys.shape.corner.none.
- **PRG-5** WHILE determinate, its separation is gap (PRG-17) and value lies strictly between min and max THE progress indicator SHALL leave comp.progress-indicator.track.gap of empty space, measured along the track, between the indicator and the rest of the track — on a bar between the indicator's end and the track, on a ring at both ends of the indicator's arc. WHILE its separation is divider THE progress indicator SHALL draw the indicator on the track with no empty space between them: the edge where the indicator's color ends is what divides them. WHILE a bar is determinate THE progress indicator SHALL draw, under either separation, a dot of comp.progress-indicator.stop-indicator.size, and no larger than the bar is thick, in comp.progress-indicator.indicator.color centered in the thickness with its center half the thickness before the track's end (concentric with the track's rounded end); it SHALL NOT be drawn while value is at max, where the indicator covers it, and a size of 0 SHALL draw none. Whether the dot is drawn is independent of the separation.
- **PRG-6** WHEN value changes while determinate THE progress indicator SHALL move the indicator's end to the new value over comp.progress-indicator.motion.value-duration with comp.progress-indicator.motion.value-easing; a switch between determinate and indeterminate SHALL take effect at once.
- **PRG-7** WHERE reduced motion is requested THE progress indicator SHALL show a value change at once, and WHILE indeterminate SHALL NOT move: it SHALL draw the indicator over the whole track (the whole circle of a ring) and ease its opacity from 1 to 0 and back to 1 once per comp.progress-indicator.motion.cycle-duration with comp.progress-indicator.motion.cycle-easing, over a track that stays still.
- **PRG-8** WHILE value is a number THE progress indicator SHALL expose the progressbar role to assistive technology with label as its name, min and max as its range, the clamped value as its current value, and valueText as the value's text when valueText is not empty.
- **PRG-9** WHILE value is null THE progress indicator SHALL expose the progressbar role with label as its name, no current value, and the platform's busy or indeterminate state.
- **PRG-10** THE progress indicator SHALL NOT be focusable and SHALL NOT announce value changes by itself; assistive technology SHALL read the current value when it reaches the indicator, and a platform that marks frequently updating elements SHALL mark it so.
- **PRG-11** IF value is below min or above max THE progress indicator SHALL draw and expose it clamped to that range; IF max is not greater than min THE progress indicator SHALL draw the track with no indicator and expose a current value of 0 in a range of 0 to 1; in each case debug builds SHALL report the values.
- **PRG-12** WHEN size names a value that the active design system does not have, or variant or separation a value the component does not define, THE progress indicator SHALL render with the default value's tokens (medium, linear, auto), and in debug builds report the unknown value.
- **PRG-13** WHERE the layout direction is right-to-left THE progress indicator SHALL fill a bar and move its indeterminate segment from right to left; a ring SHALL NOT be mirrored: it starts at the top and turns clockwise in every direction, as a clock does.
- **PRG-14** WHILE the progress indicator is not visible (scrolled out of view, in a hidden view, or in an app in the background) THE progress indicator SHALL NOT run its indeterminate animation, and SHALL resume it when it becomes visible.
- **PRG-15** WHILE label is empty THE progress indicator SHALL still render, and in debug builds report that it has no accessible name.
- **PRG-16** THE progress indicator SHALL take its layout direction from where it is used — the direction of the nearest ancestor that sets one, and the platform's own where none does — and SHALL have no direction setting of its own; WHEN that direction changes while the progress indicator is shown THE progress indicator SHALL draw itself for the new direction at once, without a motion of its own, keeping its value, and the place of an indeterminate segment in its cycle.
- **PRG-17** THE progress indicator SHALL tell a determinate indicator from the rest of its track as its separation names: WHILE separation is auto, as comp.progress-indicator.track.separation names, which follows sys.behavior.separation unless a design system sets it apart; WHILE separation is divider or gap, as that value names, whatever the tokens say; each as PRG-5 draws it. WHEN separation, or the token it follows, changes THE progress indicator SHALL draw the other form at once, without animation, keeping its value. WHILE indeterminate THE progress indicator SHALL be drawn the same under either (PRG-2, PRG-7). A platform that cannot draw a choice SHALL fall back to the first value, divider, and state that in its README.
- **PRG-18** WHERE the platform has ARIA attributes (the web), the host carries its progressbar role (PRG-8) itself, so what the product writes on it is not moved, but for a name: WHILE label is not empty THE progress indicator SHALL hold the product's aria-label and aria-labelledby on that host as their data-aria- twins, so that label is its whole name although the platform gives aria-labelledby the precedence, and WHEN label is emptied SHALL give them back as they were written; WHILE label is empty a name the product gives so — aria-label, or aria-labelledby naming an element — SHALL be its name and SHALL NOT be reported as having none in debug builds (PRG-15). WHEN the product removes one, or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaLabelledByElements) to null, THE progress indicator SHALL take it away.

### Tokens

As written in the token documents (DTCG): an alias in braces names the token it takes its value from, which each design system resolves. On the web each token is a CSS custom property: `--tanrei` and the name after `tanrei`, joined by hyphens (`tanrei.comp.progress-indicator.…` → `--tanrei-comp-progress-indicator-…`).

#### `tanrei.comp.progress-indicator`

| Token | Type | Value |
| --- | --- | --- |
| `comp.progress-indicator.linear.thickness.small` | `dimension` | `2px` |
| `comp.progress-indicator.linear.thickness.medium` | `dimension` | `4px` |
| `comp.progress-indicator.linear.thickness.large` | `dimension` | `8px` |
| `comp.progress-indicator.circular.size.small` | `dimension` | `16px` |
| `comp.progress-indicator.circular.size.medium` | `dimension` | `24px` |
| `comp.progress-indicator.circular.size.large` | `dimension` | `40px` |
| `comp.progress-indicator.circular.thickness.small` | `dimension` | `{tanrei.sys.border.thick}` |
| `comp.progress-indicator.circular.thickness.medium` | `dimension` | `{tanrei.sys.border.thick}` |
| `comp.progress-indicator.circular.thickness.large` | `dimension` | `{tanrei.sys.border.thick}` |
| `comp.progress-indicator.track.color` | `color` | `{tanrei.sys.color.surface-variant}` |
| `comp.progress-indicator.track.shape` | `dimension` | `{tanrei.sys.shape.corner.full}` |
| `comp.progress-indicator.track.separation` | `number` | `{tanrei.sys.behavior.separation}` |
| `comp.progress-indicator.track.gap` | `dimension` | `{tanrei.sys.space.space50}` |
| `comp.progress-indicator.indicator.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.progress-indicator.stop-indicator.size` | `dimension` | `{tanrei.sys.space.space0}` |
| `comp.progress-indicator.motion.value-duration` | `duration` | `{tanrei.sys.motion.duration.medium2}` |
| `comp.progress-indicator.motion.value-easing` | `cubicBezier` | `{tanrei.sys.motion.easing.standard}` |
| `comp.progress-indicator.motion.cycle-duration` | `duration` | `{tanrei.sys.motion.duration.extra-long4}` |
| `comp.progress-indicator.motion.cycle-easing` | `cubicBezier` | `{tanrei.sys.motion.easing.standard}` |

## Guidelines

### When to use

Use a progress indicator for a task that takes long enough to notice: an upload, an import, a deploy, a long save. Show how far it has come when you can measure it, even roughly; people wait more patiently for a bar that moves. When you cannot measure it yet, show it indeterminate, and switch to a value as soon as you have one.

For a short wait whose length you cannot measure, such as a list fetching its next page, use a [loading indicator](https://tanrei.chnotchy.com/components/loading-indicator/) instead. It has no track, because it never promises a value.

### Choosing a variant

| Variant | Use for |
| --- | --- |
| Linear | Tasks that belong to a page or a section: at the top of a page, inside a card, under a file in an upload list. The bar fills the width its layout gives it. |
| Circular | Tight places and progress next to text: in a button, in a list row, in a small panel. The ring has a fixed size. |

### Choosing a size

| Size | Bar thickness / ring size in the default system | Use for |
| --- | --- | --- |
| Small | 2 / 16 | Dense rows, inside controls, next to small text. |
| Medium | 4 / 24 | Most places. The default. |
| Large | 8 / 40 | The one task a view is about, or an empty section waiting for its content. |

### Divider or gap

A determinate indicator is told from the rest of its track in one of two ways. Under `divider` it is drawn on the track with no space between them. Under `gap` a little empty space is left after it, on a ring at both ends of its arc. An indeterminate indicator looks the same under both. Whether a bar marks the end of its track with a dot is a separate choice, the size of its stop indicator: a design system that gives it a size draws the dot under either separation, and the default system draws none.

The design system chooses once for every component that has this choice, and the default system chooses divider. Leave `separation` at `auto` to follow that choice, so the indicator agrees with the button groups and menus around it. Set `divider` or `gap` on one indicator only when that one has a reason to differ.

### Do

- Give it a label that says what is in progress: “Uploading report.pdf”, not “Progress”. If a caption is shown next to it, use the same words.
- Use a value text when a percentage is not the natural reading: “3 of 10 files”, “Step 2 of 4”.
- Tell people when the task ends, in words: a message that it is done, or what went wrong and what to do next.

### Don’t

- Don’t use color to report a failure. A red bar says nothing to someone who cannot see the color; stop the indicator and show a message next to it.
- Don’t use it as a gauge of a level, such as storage used or password strength. That is a [meter](https://tanrei.chnotchy.com/components/meter/), which is a different component.
- Don’t let the value go backwards or jump to the end before the task is done. The bar is a promise about the remaining wait.
- Don’t show several indicators for one task. Show one for the whole, or one for each item the person cares about.

## Accessibility

### What the component handles

- It exposes the progress bar role with the label as its name. Determinate, it exposes the range, the current value (clamped to the range) and the value text when there is one; indeterminate, it exposes no value and the platform’s busy state. Flutter has no progress bar role or busy state yet, so there it exposes the label and the value.
- It is never focusable, and it does not announce its changes. Assistive technology reads the current value when the person reaches it.
- When reduced motion is requested, a value change shows at once, and the indeterminate form stops moving and gently pulses its opacity instead, at most once a second.
- Its looping animation stops while it cannot be seen — in an app in the background everywhere, and scrolled out of view or on a hidden screen where the platform can tell — and resumes when it can.
- An empty label, a value outside the range and an unknown `separation` are reported in debug builds.

### What you need to do

- Announce the end of the task yourself: a message in a live region, or focus moved to the result. The indicator will not say that the task is done or has failed.
- Keep the indicator color at least 3:1 against the track and the surface behind it if you change them, with a divider and with a gap alike. The value is read from where the indicator meets the track.
- On a surface of the highest container tone, such as a filled card, the track is hard to see: it has nearly the same tone. Where an indicator stands on one, point `comp.progress-indicator.track.color` at a stronger role in a layer. What the indicator shows is still read where it meets the track.
- If the task holds up a region of the page, mark that region busy while it runs (`aria-busy` on the web).
