UIData Displayno dependencies

ProgressRing

A circular SVG progress indicator whose arc and center percentage tween smoothly to each new value, with an optional two-color gradient stroke, soft glow, rounded caps and a caption.

Preview

0%Storage used

Customize

11 props
72
100
168
12
1200

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add progress-ring

Writes components/ui/progress-ring.tsx and components/styles/progress-ring.module.css.

Usage

Updates live as you change the controls — only props that differ from the defaults are included.

example.tsx
1import { ProgressRing } from "@motionfoundry/ui";2 3export function Example() {4  return (5    <ProgressRing />6  );7}

Props

PropTypeDefault
value

Current progress, clamped between 0 and max; changes tween smoothly.

number72
max

Value representing a full ring.

number100
size

Outer diameter in pixels.

number168
thickness

Stroke thickness in pixels.

number12
color

Progress stroke color (gradient start).

string'#6366f1'
color2

Gradient end color, used when gradient is true.

string'#22d3ee'
gradient

Blend color into color2 along the arc.

booleantrue
trackColor

Track color; defaults to the theme border color.

stringundefined
label

Caption under the number; also the accessible name.

string'Storage used'
showValue

Show the percentage in the center.

booleantrue
roundedCaps

Round the ends of the progress arc.

booleantrue
duration

Tween duration in milliseconds (0 = instant).

number1200
children

Custom center content replacing the number and caption.

React.ReactNodeundefined
className

Additional class merged onto the root element.

string''
style

Inline styles merged onto the root element.

React.CSSPropertiesundefined

Accessibility

The root has role="progressbar" with aria-valuemin, aria-valuemax, aria-valuenow (the target, not the tweening value), aria-valuetext as a percentage and aria-label from the caption. The SVG is aria-hidden. Under prefers-reduced-motion the value jumps without tweening.

Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.

Source

components/ui/progress-ring.tsx
1"use client";2 3import * as React from "react";4import styles from "../styles/progress-ring.module.css";5 6export interface ProgressRingProps {7  /** Current progress, clamped between 0 and `max` */8  value?: number;9  /** Value that represents a full ring */10  max?: number;11  /** Outer diameter in pixels */12  size?: number;13  /** Stroke thickness in pixels */14  thickness?: number;15  /** Progress stroke color */16  color?: string;17  /** Second color for a gradient stroke (used when `gradient` is true) */18  color2?: string;19  /** Blend `color` into `color2` along the arc */20  gradient?: boolean;21  /** Track color (defaults to the theme border color) */22  trackColor?: string;23  /** Caption shown under the number */24  label?: string;25  /** Show the numeric percentage in the center */26  showValue?: boolean;27  /** Round the ends of the progress arc */28  roundedCaps?: boolean;29  /** Tween duration in milliseconds (0 = instant) */30  duration?: number;31  /** Optional custom center content (replaces the number + caption) */32  children?: React.ReactNode;33  /** Optional className merged onto the root */34  className?: string;35  /** Optional inline styles merged onto the root */36  style?: React.CSSProperties;37}38 39const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3);40 41export function ProgressRing({42  value = 72,43  max = 100,44  size = 168,45  thickness = 12,46  color = "#6366f1",47  color2 = "#22d3ee",48  gradient = true,49  trackColor,50  label = "Storage used",51  showValue = true,52  roundedCaps = true,53  duration = 1200,54  children,55  className = "",56  style,57}: ProgressRingProps) {58  const id = React.useId().replace(/:/g, "");59  const safeMax = max > 0 ? max : 100;60  const target = Math.min(safeMax, Math.max(0, value));61  const [shown, setShown] = React.useState(0);62  const shownRef = React.useRef(0);63 64  React.useEffect(() => {65    const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;66    const from = shownRef.current;67    if (reduce || duration <= 0 || from === target) {68      shownRef.current = target;69      setShown(target);70      return;71    }72    let raf = 0;73    let start = 0;74    const tick = (now: number) => {75      if (!start) start = now;76      const t = Math.min(1, (now - start) / duration);77      const v = from + (target - from) * easeOutCubic(t);78      shownRef.current = v;79      setShown(v);80      if (t < 1) raf = requestAnimationFrame(tick);81    };82    raf = requestAnimationFrame(tick);83    return () => cancelAnimationFrame(raf);84  }, [target, duration]);85 86  const stroke = Math.max(1, Math.min(thickness, size / 2 - 1));87  const r = (size - stroke) / 2;88  const c = 2 * Math.PI * r;89  const pct = shown / safeMax;90  const offset = c * (1 - pct);91  const percent = Math.round(pct * 100);92  const strokeColor = gradient ? `url(#${id}-g)` : color;93 94  return (95    <div96      role="progressbar"97      aria-valuemin={0}98      aria-valuemax={safeMax}99      aria-valuenow={Math.round(target)}100      aria-valuetext={`${Math.round((target / safeMax) * 100)}%`}101      aria-label={label || "Progress"}102      className={`${styles.root} ${className}`.trim()}103      style={{ "--mf-pr-size": `${size}px`, "--mf-pr-color": color, ...style } as React.CSSProperties}104    >105      <svg aria-hidden="true" className={styles.svg} width={size} height={size} viewBox={`0 0 ${size} ${size}`}>106        {gradient && (107          <defs>108            <linearGradient id={`${id}-g`} x1="0" y1="0" x2="1" y2="1">109              <stop offset="0%" stopColor={color} />110              <stop offset="100%" stopColor={color2} />111            </linearGradient>112          </defs>113        )}114        <circle115          className={styles.track}116          cx={size / 2}117          cy={size / 2}118          r={r}119          fill="none"120          strokeWidth={stroke}121          style={trackColor ? { stroke: trackColor } : undefined}122        />123        <circle124          className={styles.bar}125          cx={size / 2}126          cy={size / 2}127          r={r}128          fill="none"129          stroke={strokeColor}130          strokeWidth={stroke}131          strokeLinecap={roundedCaps && pct > 0.002 ? "round" : "butt"}132          strokeDasharray={c}133          strokeDashoffset={offset}134          transform={`rotate(-90 ${size / 2} ${size / 2})`}135        />136      </svg>137      <div className={styles.center}>138        {children ?? (139          <>140            {showValue && (141              <span className={styles.value}>142                {percent}143                <span className={styles.unit}>%</span>144              </span>145            )}146            {label && <span className={styles.label}>{label}</span>}147          </>148        )}149      </div>150    </div>151  );152}