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
'use client';import { Progress } from '@gremorie/rx-feedback';import { useEffect, useState } from 'react';export function ProgressPreview() { const [value, setValue] = useState(13); useEffect(() => { const id = setTimeout(() => setValue(72), 500); return () => clearTimeout(id); }, []); return ( <div className="flex max-w-md flex-col gap-3"> <Progress value={value} /> <Progress value={33} /> <Progress value={88} /> </div> );}Anatomy
Progress a rounded track containing a filled indicator translated by value%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>
);
}npx gremorie@latest add ng-progressimport { Component, input } from '@angular/core';
import { Progress } from '@gremorie/ng-feedback';
@Component({
selector: 'app-upload-progress',
standalone: true,
imports: [Progress],
template: `
<div class="flex flex-col gap-2">
<div class="flex justify-between text-sm text-muted-foreground">
<span>Uploading</span>
<span>{{ value() }}%</span>
</div>
<gr-progress [value]="value()" />
</div>
`,
})
export class UploadProgressComponent {
readonly value = input<number>(0);
}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".
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | 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). |
max | number | 100 | Upper 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) => string | percentage | Custom label generator for aria-valuetext. Override to localize ("42 percent") or to expose units ("3 of 8 steps"). |
className | string | - | Applied to the root track. Override height with care - 8 px (h-2) matches the rest of the surface. |
...props | React.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
<Progress value={n} />is the whole component - there is no separate<ProgressIndicator>slot to compose at the call site.- 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. - Inside an
Alertor aCard: 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
Value labels
'use client';import { Progress } from '@gremorie/rx-feedback';export function ProgressValuesPreview() { return ( <div className="flex max-w-md flex-col gap-4"> {[0, 33, 66, 100].map((value) => ( <div key={value} className="flex flex-col gap-2"> <div className="flex justify-between text-sm"> <span>Step {value === 0 ? 'queued' : `${value}%`}</span> <span className="text-muted-foreground">{value}%</span> </div> <Progress value={value} /> </div> ))} </div> );}The same bar across the full determinate range - 0, 33, 66, and 100. Pair every bar with its numeric value so the fill is never silent: at 0 the user sees the task is queued, at 100 that it is done. value is clamped between 0 and max (default 100).
With label
<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}", andaria-valuenow="{value}"automatically. Screen readers announce the percentage on every change. - Value label: override
getValueLabelfor 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
Sliderinstead. - Indeterminate state: pass
value={null}to droparia-valuenowso assistive tech announces the task as in progress without a misleading number. - Reduced motion: the indicator uses
transition-allfor smooth fills. Users withprefers-reduced-motion: reduceautomatically get the global motion override - no extra config needed.