Base components
Progress bar
A horizontal bar that shows how much of a task is done, where the fill glides to each new value and the percent label rolls to the same number.
Live preview
Loading preview
Four bars, one per label position, each at 40%, and a file upload row "report.pdf" whose bar goes 0, 35, 70 and 100%. Replay runs the upload again from 0.
Installation
Run this in your app after the one-time setup on the Getting started page.
$ npx @ganisigit/lunia-ds add progress-barUsage
The demo above, as code.
demo.tsx
'use client';
import * as React from 'react';
import { ProgressBar } from './progress-bar';
const STEPS = [0, 35, 70, 100];
const STEP_MS = 900;
export default function ProgressBarDemo({ replay = 0 }: { replay?: number }) {
const [step, setStep] = React.useState(STEPS.length - 1);
React.useEffect(() => {
if (replay === 0) return;
setStep(0);
const timers = STEPS.slice(1).map((_, i) => window.setTimeout(() => setStep(i + 1), (i + 1) * STEP_MS));
return () => {
for (const t of timers) window.clearTimeout(t);
};
}, [replay]);
return (
<div className="flex w-full max-w-xs flex-col gap-8">
<ProgressBar value={40} label="right" aria-label="Profile complete" />
<ProgressBar value={40} label="bottom" aria-label="Course progress" />
<ProgressBar value={40} label="top-floating" aria-label="Storage used" />
<ProgressBar value={40} label="bottom-floating" aria-label="Tasks done" />
<div className="flex flex-col gap-3 rounded-xl border border-secondary bg-primary p-4">
<div className="flex items-baseline justify-between gap-3">
<span id="upload-file" className="text-sm font-medium text-secondary">
report.pdf
</span>
<span className="text-sm text-tertiary">2.4 MB</span>
</div>
<ProgressBar value={STEPS[step]} label="right" aria-labelledby="upload-file" />
</div>
</div>
);
}
API
Parts: root, track, fill (hidden at 0%; at least an 8px dot above 0%), value label (right, bottom, or a floating label on the end of the fill).
| Prop | Type | Default | Description |
|---|---|---|---|
| value | number | 0 | Progress from min to max. |
| min | number | 0 | Lowest value. |
| max | number | 100 | Highest value. |
| label | "none" | "right" | "bottom" | "top-floating" | "bottom-floating" | "none" | Where the value label shows: none, right of the bar, under its end, or in a floating label on the end of the fill. |
| format | (value: number, max: number) => string | percent, for example "40%" | Text of the value label and of aria-valuetext. |
| animateOnMount | boolean | true | Grows the fill from 0 to the first value when the bar first shows. |
Motion
Every row names a token from the Motion page. The last column says what happens when the user asks for reduced motion.
| ID | Trigger | Element | Property | From → to | Timing | Reduced motion |
|---|---|---|---|---|---|---|
| PB1 | value changes | fill | width (scale x from the start edge) | old value → new value | duration-5xl (640)ease-out | Resizes at once |
| PB2 | value changes | value label number | number roll (number-roll) | old percent → new percent | duration-5xl (640)ease-out | Swaps at once |
| PB3 | value changes (floating labels) | floating label | translate x (stays centered on the end of the fill) | old end → new end | duration-5xl (640)ease-out | Moves at once |
| PB4 | first time the bar is in view (animateOnMount) | fill, label | width and number roll from 0 (as PB1 to PB3) | 0 → value | duration-5xl (640)ease-out | Shows at once |
Accessibility
- Built on a progress primitive: role progressbar with aria-valuenow, aria-valuemin, aria-valuemax and aria-valuetext from format.
- Needs a name: aria-label or aria-labelledby pointing at the visible title, for example "Upload progress".
- The rolling label is aria-hidden during the roll; screen readers read aria-valuetext, not each frame.
- Do not announce every change; a parent that needs it adds a polite live region at key points (for example done).
- Reduced motion: the fill and the label jump to the new value.