Progress
How far along a task is, or that it is running at all.
import { Div, ImplementLifecycle, signal, Span } from "@implementjs/core";
import { Progress } from "@/lib/components/ui/progress";
export default function ProgressDemo() {
const value = signal<number | null>(13);
return Div(
{ class: "flex w-full max-w-sm flex-col gap-2" },
ImplementLifecycle({
onMount: () => {
const timer = setInterval(() => {
value.update((v) => (v === null || v >= 100 ? 13 : Math.min(100, v + 29)));
}, 1000);
return () => clearInterval(timer);
},
}),
Div(
{ class: "flex items-center justify-between text-sm font-medium" },
Span({ id: "upload-label" }, "Uploading photos"),
Span(
{ class: "tabular-nums" },
value.bind((v) => `${v ?? 0}%`),
),
),
Progress({ "aria-labelledby": "upload-label", value }),
);
}Installation
npx jsrepo add @implementjs/ui/progress
Nothing else comes with it — this one stands alone on @implementjs/core and @implementjs/primitives.
Copy the file below to src/lib/components/ui/progress.ts. It imports cn, so copy utils.ts to src/lib/utils.ts too.
import { derived, Div, signal, type ComponentProps } from "@implementjs/core";
import { Progress as ProgressPrimitive } from "@implementjs/primitives";
import { createComponent } from "@implementjs/primitives";
import { cn } from "@/lib/utils";
export type ProgressProps = ComponentProps<typeof ProgressPrimitive>;
export const Progress = createComponent(function Progress({
class: className,
value = 0,
min = 0,
max = 100,
...props
}: ProgressProps) {
const valueSignal = signal(value);
const minSignal = signal(min);
const maxSignal = signal(max);
// An indeterminate bar shows a full-width pulsing fill instead of a position.
const percent = derived([valueSignal, minSignal, maxSignal], (value, min, max) => {
if (value === null || max === min) return 100;
return Math.min(100, Math.max(0, ((value - min) / (max - min)) * 100));
});
return ProgressPrimitive(
{
value: valueSignal,
min: minSignal,
max: maxSignal,
...props,
"data-slot": "progress",
class: cn(
"group/progress relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
className,
),
},
Div({
"data-slot": "progress-indicator",
class:
"size-full flex-1 bg-primary transition-transform group-data-[indeterminate]/progress:animate-pulse",
style: { transform: percent.bind((p) => `translateX(-${100 - p}%)`) },
}),
);
});Usage
A task that starts and finishes: an upload, an import, a build. For a standing measurement, use meter.
The wrapper renders the indicator and drives it from value, min, and max. Pass value: null for a task whose length is unknown — the bar fills and pulses instead of showing a position.
import { signal } from "@implementjs/core";
import { Progress } from "@/lib/components/ui/progress";
const value = signal(13);
Progress({ value, "aria-label": "Uploading photos" });
Indeterminate
Progress({ value: null, "aria-label": "Importing" });
The primitive sets data-indeterminate on the root, and the indicator animates on it. motion-reduce is respected through Tailwind's own variant on the transition.
API Reference
Every prop the styling does not consume is forwarded to the Progress primitive, so the tables below are the whole surface — the behavior props and the styling ones together.
Progress
Completion status of a task. Sets role="progressbar" and the aria value attributes. Give it a track and a fill; it handles the semantics. Styled as a rounded track. The wrapper renders the indicator inside it; an indeterminate bar pulses at full width instead of showing a position. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
value | Signal<number | null> | number | null | 0 | The current value; null renders an indeterminate bar. Pass a signal to control it from outside; a number seeds uncontrolled state. |
min | Signal<number> | number | 0 | The value the bar starts from. |
max | Signal<number> | number | 100 | The value at which the task is complete. |
| Data attribute | Value |
|---|---|
[data-progress-root] | Present |
[data-state] | "loading" | "loaded" | "indeterminate" |
[data-value] | The current value; absent while indeterminate |
[data-min] | The minimum value |
[data-max] | The maximum value |
[data-indeterminate] | Present while the value is null |