Progress ring

Free · MITReact Native · Expo

A circular progress ring that fills on a spring, laps itself past 100 % with a gap that lifts the lap off the ring, and bounces once with a success haptic when it completes.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add https://goodui.dev/native/r/progress-ring.json

Then run npx expo install --fix so react-native-reanimated, react-native-svg match your Expo SDK. Files land in components/goodui-native/progress-ring.

Usage

import { ProgressRing } from "@/components/goodui-native/progress-ring/progress-ring";
import { Text } from "@/components/goodui-native/foundation/text";

export function StepsGoal({ steps }: { steps: number }) {
  return (
    <ProgressRing value={steps / 10000} size="lg" accessibilityLabel="Steps">
      <Text size="title1" weight="medium" tabular>{steps.toLocaleString()}</Text>
      <Text size="footnote" tone="secondary">of 10,000 steps</Text>
    </ProgressRing>
  );
}

When to use

Use it for
  • Goals and quotas: steps, move minutes, savings targets, storage used, course completion.
  • Several related goals at once: pass rings for activity-rings style concentric progress.
  • Determinate tasks with a known fraction, like an upload, when the ring sits next to its label.
Reach for something else
  • Waiting without a known fraction: use the Spinner from the foundation, or a button that returns its promise.
  • Placeholders while content loads: use skeleton.
  • Trends over time: use sparkline.

Examples

Activity rings

<ProgressRing
  size="lg"
  rings={[
    { label: "Move", value: 1.35, tone: "danger" },
    { label: "Exercise", value: 0.72, tone: "success" },
    { label: "Stand", value: 0.5, tone: "accent" },
  ]}
/>

Rolling percentage in the center

<ProgressRing value={uploaded / total} showValue tone="success" accessibilityLabel="Upload" />

Small, inline

<ProgressRing value={0.4} size="sm" tone="warning" accessibilityLabel="Storage" />

API reference

PropTypeDefault
value
Progress of a single ring, 0 to 1. Above 1 the ring laps itself. Ignored when `rings` is set.
number0
rings
Concentric rings, outermost first, activity-rings style.
ProgressRingSpec[]—
size
Diameter: `sm` 44, `md` 96, `lg` 168, or a number in points.
"sm" | "md" | "lg" | number"md"
thickness
Stroke width. Defaults to about a twelfth of the diameter.
number—
tone
Color of a single ring, and the default for `rings`.
"success" | "warning" | "danger" | "accent" | "neutral""accent"
children
Center content. Replaces `showValue`.
ReactNode—
showValue
Show the first ring's percentage in the center as a rolling number.
booleanfalse
animateOnMount
Draw the arcs in from zero when the ring mounts. Default `true`.
booleantrue
haptic
Haptic when a ring reaches 100 % after mount. `false` turns it off.
HapticName | false"success"
on
The surface the ring sits on. The gap that separates a lap from the ring under it uses this color.
"surface" | "background""surface"
accessibilityLabel
Label for a single ring, for example "Upload". Rings use their own labels. Same as `labels.progress`.
string—
labels
Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide.
Partial<ProgressRingLabels>—
style
Style for the outer container.
StyleProp<ViewStyle>—
testID
Identifier for end-to-end tests.
string—

Motion and haptics

  • Arcs fill on the smooth spring; on mount they draw in from zero, outer ring first, 90 ms apart.
  • Past 100 % a lap arc draws on top, and a disc in the surface color under its head separates it from the ring below.
  • Crossing 100 % after mount scales the ring to 1.06 and back on the bouncy spring, once.
  • The center percentage rolls with rolling-number.
  • Reduced motion: arcs jump to their value, there is no bounce, and the number changes without rolling.
  • Haptic: success when a ring crosses 100 % after mount (configurable with the haptic prop); never on mount
  • Arc length, lap and gap position are animated props on the UI thread; React renders only when values change.

Accessibility

  • accessibilityRole is progressbar; a single ring reports accessibilityValue from 0 to 100.
  • The label lists every ring with its percentage, for example 'Move 135 percent, Exercise 72 percent'.
  • Reaching 100 % is announced as '<label> complete'.
  • Center content is hidden from screen readers because the ring's label already carries the value.

Notes for AI

  • value is a fraction, not a percent: pass 0.72, not 72.
  • Set on="background" when the ring sits on the screen background instead of a card, so the lap gap matches.
  • For activity rings pass rings (outermost first) with a label each; value and accessibilityLabel are then ignored.
  • Put your own text in children for units like 'of 10,000 steps'; showValue only renders the percentage.