implement
Progress

Progress

Show how far a task has advanced toward completion.

Uploading photos13%
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 }),
	);
}

A progress bar shows the completion status of a task — a file upload, an installation, a multi-step form. Progress renders a Div with role="progressbar" and the aria value attributes; you draw the track and the fill.

import { Progress } from "@implementjs/primitives";

Progress({ value: 40, "aria-label": "Uploading" });

It accepts optional props and children — pass a props object when you need attributes, or pass children directly. See createComponent. Extra props are forwarded onto the underlying Div.

A progress bar's value only ever advances toward completion. For a measurement that can move in either direction — CPU usage, battery level — use Meter instead; assistive technology announces the two differently.

Value and range

value defaults to 0 inside a range from min (0) to max (100). Pass numbers to seed them, or a signal to control them from outside (signal() returns a writable unchanged, so the same prop accepts both):

const uploaded = signal(0);

Progress({ value: uploaded, "aria-label": "Uploading" });

uploaded.set(40); // the aria and data attributes follow

Indeterminate

When you cannot know how far along the task is, pass null. The bar drops aria-valuenow, sets data-state="indeterminate", and adds a bare data-indeterminate attribute for styling:

Progress({ value: null, "aria-label": "Loading" });

A signal holding number | null can move between the two — start indeterminate while a size is unknown, then switch to real values.

Accessibility

The primitive sets role="progressbar", aria-valuemin, aria-valuemax, and aria-valuenow (omitted while indeterminate). Two things are left to you:

  • A name. If there is a visible label, point aria-labelledby at its id; otherwise pass aria-label.
  • A readable value. Screen readers often announce aria-valuenow as a percentage. When a percentage is not how a person would say the value, pass aria-valuetext — an installer might use "step 2 of 5".
(Span({ id: "install-label" }, "Installing"),
	Progress({
		"aria-labelledby": "install-label",
		"aria-valuetext": "step 2 of 5",
		value: 2,
		max: 5,
	}));

Styling

The primitive is invisible until you style it — it has no default size or color. Style the root as the track and put your own fill inside it. data-value, data-min, and data-max are on the root for CSS to react to, and data-state moves through "loading", "loaded", and "indeterminate":

const uploaded = signal(40);

Progress(
	{
		value: uploaded,
		"aria-label": "Uploading",
		class: "h-2 w-56 overflow-hidden rounded-full bg-muted data-[state=loaded]:opacity-50",
	},
	Div({
		class: "h-full bg-primary transition-[width]",
		style: { width: uploaded.bind((v) => `${v}%`) },
	}),
);

data-indeterminate is present only while the value is null, so one selector can swap the fill for a looping animation: data-indeterminate:animate-pulse on the root, or group-data-[indeterminate]:animate-slide on the fill.

API Reference

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. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
valueSignal<number | null> | number | null0The current value; null renders an indeterminate bar. Pass a signal to control it from outside; a number seeds uncontrolled state.
minSignal<number> | number0The value the bar starts from.
maxSignal<number> | number100The value at which the task is complete.
Data attributeValue
[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