Lunia DS

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.

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-bar

Usage

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).

PropTypeDefaultDescription
valuenumber0Progress from min to max.
minnumber0Lowest value.
maxnumber100Highest 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) => stringpercent, for example "40%"Text of the value label and of aria-valuetext.
animateOnMountbooleantrueGrows 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.

IDTriggerElementPropertyFrom → toTimingReduced motion
PB1value changesfillwidth (scale x from the start edge)old value → new valueduration-5xl (640)ease-outResizes at once
PB2value changesvalue label numbernumber roll (number-roll)old percent → new percentduration-5xl (640)ease-outSwaps at once
PB3value changes (floating labels)floating labeltranslate x (stays centered on the end of the fill)old end → new endduration-5xl (640)ease-outMoves at once
PB4first time the bar is in view (animateOnMount)fill, labelwidth and number roll from 0 (as PB1 to PB3)0 → valueduration-5xl (640)ease-outShows 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.