Skip to main content
Gremorie

Progress

Determinate horizontal progress bar with token-driven track and indicator.

Overview

Progress is the determinate progress primitive: a horizontal bar that fills from 0 to 100 percent as a long-running task advances. Wraps Radix Progress so the underlying aria-valuenow, aria-valuemin, aria-valuemax, and role="progressbar" semantics are handled for you.

Use Progress when percent complete is known: uploads with byte counts, multi-step forms with explicit steps, batch jobs reporting from the server. For unknown durations, reach for Skeleton or a spinner instead - showing a determinate bar that does not actually correlate with progress feels worse than no bar at all.

Preview

Installation

bash npx gremorie@latest add rx-progress
bash pnpm dlx gremorie@latest add rx-progress
bash yarn dlx gremorie@latest add rx-progress
bash bunx --bun gremorie@latest add rx-progress

Usage

import { Progress } from "@gremorie/rx-feedback";

export function UploadProgress({ value }) {
  return (
    <div className="flex flex-col gap-2">
      <div className="flex justify-between text-sm text-muted-foreground">
        <span>Uploading</span>
        <span>{value}%</span>
      </div>
      <Progress value={value} />
    </div>
  );
}

Angular edition planned for a follow-up release. The value contract will map directly to a one-way input on a structural directive.

API

<Progress>

The root and indicator together. Renders a Radix Root styled as a 8 px track plus an Indicator that translates horizontally based on value. The track and indicator slots are exposed via data-slot="progress" and data-slot="progress-indicator".

PropTypeDefaultDescription
valuenumber | null-Current value, 0 to 100. Drives the indicator translation. Set to null to enter the indeterminate state (the indicator stops responding to value and you take over via a custom class).
maxnumber100Upper bound. Most callers leave this at 100 and pass a percentage; pass a different max (e.g. file bytes) only when the source value is naturally in that range.
getValueLabel(value: number, max: number) => stringpercentageCustom label generator for aria-valuetext. Override to localize ("42 percent") or to expose units ("3 of 8 steps").
classNamestring-Applied to the root track. Override height with care - 8 px (h-2) matches the rest of the surface.
...propsReact.ComponentProps<typeof ProgressPrimitive.Root>-All Radix Root attributes.

The bundled indicator is determinate. To build an indeterminate variant, pass value={null} and add a custom class on the indicator (or fork the component) to animate a moving stripe. Showing an indeterminate bar with a fake animated value is a tracking-by-illusion anti-pattern.

Composition

  1. <Progress value={n} /> is the whole component - there is no separate <ProgressIndicator> slot to compose at the call site.
  2. Pair with a label: a silent bar leaves the user guessing. Mount a small <span> above or beside it with the percent, the step count, or the byte progress.
  3. Inside an Alert or a Card: progress that explains what is loading reads better than progress that floats alone. Wrap with a context line so users know why they are waiting.

Variations

With label

Uploading invoice.pdf66%
<div className="flex flex-col gap-2">
  <div className="flex justify-between text-sm">
    <span>Uploading {file.name}</span>
    <span className="text-muted-foreground">{value}%</span>
  </div>
  <Progress value={value} />
</div>

The default shape for any long-running task with a known total. Keep the value text close to the bar so the two read as one widget.

Stacked steps

{
  steps.map((step) => <Progress key={step.id} value={step.percent} />);
}

For multi-stream tasks (parallel uploads, batch jobs reporting per-item progress). Pair each row with a per-row label outside the bar.

Thin bar inside a card header

<Card>
  <CardHeader>
    <CardTitle>Sync</CardTitle>
    <Progress value={value} className="h-1" />
  </CardHeader>
  <CardContent>{content}</CardContent>
</Card>

Drop the height to h-1 when the bar sits at the top of a card as a subtle progress sliver rather than a primary surface.

Accessibility

  • Role and value: Radix sets role="progressbar", aria-valuemin="0", aria-valuemax="{max}", and aria-valuenow="{value}" automatically. Screen readers announce the percentage on every change.
  • Value label: override getValueLabel for localized announcements or non-percentage units. Default is "{value}/{max}"-style.
  • No keyboard interaction: Progress is non-interactive. If you need a draggable scrubber, reach for Slider instead.
  • Indeterminate state: pass value={null} to drop aria-valuenow so assistive tech announces the task as in progress without a misleading number.
  • Reduced motion: the indicator uses transition-all for smooth fills. Users with prefers-reduced-motion: reduce automatically get the global motion override - no extra config needed.
  • Skeleton - reach for Skeleton for unknown-duration fetches with no determinate value.
  • Alert - sibling feedback primitive for context-setting messages while progress runs.
  • Slider - sibling primitive with a draggable thumb when the user controls the value.

On this page