UILoaders and Feedbackmotion/react

ProgressBar

A labelled progress bar whose fill springs to each new value with a glowing leading edge, a sheen sweeping along it, and a percentage that counts with it.

Preview

Progress

Customize

10 props
65
10
120
100
360

Installation

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

npx motionfoundry add progress-bar

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

Usage

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

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

Props

PropTypeDefault
value

Current value.

number65
max

Maximum value.

number100
label

Label above the bar; empty hides the header label.

string'Progress'
showValue

Show the percentage readout.

booleantrue
color

Fill color.

string'#ff8a1f'
trackColor

Track color.

stringtheme raised surface
height

Bar thickness in pixels.

number10
sheen

Moving sheen across the fill.

booleantrue
stiffness

Spring stiffness; lower is bouncier and slower.

number120
autoPlay

Demo mode: cycle through sample values.

booleanfalse
width

Width in pixels.

number360
className

Merged onto the root.

string''
style

Merged onto the root.

React.CSSPropertiesundefined

Accessibility

Uses role="progressbar" with aria-valuemin/max/now and an aria-valuetext percentage, labelled by its visible label. The animated readout is aria-hidden so values aren't announced mid-animation. Reduced motion jumps straight to the value and removes the sheen.

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-bar.tsx
1"use client";2 3import * as React from "react";4import { animate, motion, useMotionValue, useReducedMotion, useSpring, useTransform } from "motion/react";5import styles from "../styles/progress-bar.module.css";6 7export interface ProgressBarProps {8  /** Current value */9  value?: number;10  /** Maximum value */11  max?: number;12  /** Label shown above the bar (empty hides the header) */13  label?: string;14  /** Show the percentage readout */15  showValue?: boolean;16  /** Fill color */17  color?: string;18  /** Track color */19  trackColor?: string;20  /** Bar thickness in pixels */21  height?: number;22  /** Moving sheen across the fill */23  sheen?: boolean;24  /** Spring stiffness — lower is bouncier and slower */25  stiffness?: number;26  /** Demo mode: keep moving between sample values to show the spring */27  autoPlay?: boolean;28  /** Width in pixels */29  width?: number;30  /** Optional className merged onto the root */31  className?: string;32  /** Optional inline styles merged onto the root */33  style?: React.CSSProperties;34}35 36const DEMO_VALUES = [65, 28, 92, 46];37 38export function ProgressBar({39  value = 65,40  max = 100,41  label = "Progress",42  showValue = true,43  color = "#ff8a1f",44  trackColor,45  height = 10,46  sheen = true,47  stiffness = 120,48  autoPlay = false,49  width = 360,50  className = "",51  style,52}: ProgressBarProps) {53  const reduceMotion = useReducedMotion();54  const labelId = React.useId();55  const [demoIndex, setDemoIndex] = React.useState(-1);56  const shown = autoPlay && demoIndex >= 0 ? (DEMO_VALUES[demoIndex] / 100) * max : value;57  const pct = max > 0 ? Math.min(100, Math.max(0, (shown / max) * 100)) : 0;58 59  const raw = useMotionValue(0);60  const spring = useSpring(raw, { stiffness, damping: Math.max(8, stiffness / 9), mass: 0.9 });61  const widthPct = useTransform(spring, (v) => `${Math.min(100, Math.max(0, v))}%`);62  const readout = React.useRef<HTMLSpanElement>(null);63 64  React.useEffect(() => {65    if (reduceMotion) {66      raw.jump(pct);67      spring.jump(pct);68    } else raw.set(pct);69    // Count the readout along with the spring.70    const controls = animate(Number(readout.current?.dataset.value ?? 0), pct, {71      duration: reduceMotion ? 0 : 0.8,72      ease: [0.22, 1, 0.36, 1],73      onUpdate: (v) => {74        if (readout.current) {75          readout.current.textContent = `${Math.round(v)}%`;76          readout.current.dataset.value = String(v);77        }78      },79    });80    return () => controls.stop();81  }, [pct, reduceMotion, raw, spring]);82 83  React.useEffect(() => {84    if (!autoPlay || reduceMotion) return;85    setDemoIndex(0);86    const id = window.setInterval(() => setDemoIndex((i) => (i + 1) % DEMO_VALUES.length), 1800);87    return () => window.clearInterval(id);88  }, [autoPlay, reduceMotion]);89 90  return (91    <div92      className={`${styles.root} ${className}`.trim()}93      style={94        {95          "--mf-pb-color": color,96          "--mf-pb-height": `${height}px`,97          "--mf-pb-width": `${width}px`,98          ...(trackColor ? { "--mf-pb-track": trackColor } : null),99          ...style,100        } as React.CSSProperties101      }102    >103      {(label || showValue) && (104        <div className={styles.header}>105          <span id={labelId}>{label}</span>106          {showValue && (107            <span ref={readout} className={styles.value} aria-hidden="true" data-value="0">108              0%109            </span>110          )}111        </div>112      )}113      <div114        role="progressbar"115        aria-labelledby={label ? labelId : undefined}116        aria-label={label ? undefined : "Progress"}117        aria-valuemin={0}118        aria-valuemax={max}119        aria-valuenow={Math.round(shown)}120        aria-valuetext={`${Math.round(pct)}%`}121        className={styles.track}122      >123        <motion.div className={styles.fill} style={{ width: widthPct }}>124          {sheen && !reduceMotion && <span className={styles.sheen} aria-hidden="true" />}125        </motion.div>126      </div>127    </div>128  );129}