Progress

A bar that reports how far along something is. Built on Radix Progress, so the value is announced to assistive tech for free. The label and readout are droppable parts, and a chrome variant strips the track.

Installation#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add progress

Usage#

Pass value (0 to max, which defaults to 100). That alone renders the bare bar. Anything you pass as children becomes the row above it, so a label and a readout are composed in rather than switched on: there is no showLabel prop, and dropping either part simply removes it.

usage.tsxTSX
import { Progress, ProgressLabel, ProgressValue } from "@/components/ui/progress"export function Example() {  return (    <Progress value={72}>      <ProgressLabel>Importing components</ProgressLabel>      <ProgressValue />    </Progress>  )}

With a label and a readout#

ProgressValue prints a whole percentage by default and reads the value from context, so it never drifts from the bar. Give it a function to phrase the readout yourself: {(value, max) => …} receives the same numbers. It is aria-hidden, because Radix already exposes the value on the bar itself and a screen reader should not hear it twice.

Importing components

Sizes#

Four heights. xs (4px) is page chrome: the reading or loading bar pinned under a header. sm is the default for a bar sitting in content, md and lg for a bar that is the point of the panel it is in.

xs
sm
md
lg

Tones#

The fill takes any of the semantic roles, so a bar can report health as well as distance: flip to success when a task lands, destructive when a quota is nearly spent. Every tone is a token, so all four themes track it.

Brand
Success
Warning
Destructive
Info
Foreground

Vertical#

orientation="vertical" stands the bar up for the things that fill from the floor: a tank, a level meter, a quota per volume. The fill rises from the bottom, size sets the bar’s width instead of its height, and the header parts stack centred above it, so ProgressLabel still names the bar. The bar is as tall as the root: give it a height with className (it is 128px otherwise), or let its parent size it, a column flex parent included: h-auto flex-1 takes what the column leaves. The semantics do not change: the root is still a Radix progressbar reading the value, and it carries data-orientation as a styling hook. There is no aria-orientation, since ARIA does not allow it on a progressbar.

Tank 1
Tank 2
Tank 3
Tank 4

For a tank gauge rather than a line, restyle the track through its data-slot: it fills the root’s width and becomes a ringed well, and padding on it insets the fill, which rises as a share of the well’s inner height (no calc() to write). Step the radii down concentrically: a 24px well with an 8px inset holds a 16px fill. The fill takes the track’s corners, so without the inset a low level in a round-cornered track is carved by the track’s own outline; the inset keeps it a clean pill at any height. Here each gauge is a flex item of a column, taking whatever height the column leaves it. The fill and the indeterminate sweep work the same at any width.

Diesel
Unleaded
HVO

On a muted ground#

The track is a relative tint of the foreground rather than the opaque muted token, so it always sits one step off whatever is behind it: on a card it reads as the muted groove it always was, and on a muted ground (a selected table row, a muted panel) it still shows, where a muted groove would vanish into the row. Nothing to set: it is how the track paints.

  • Intake form
  • Vitals
  • Consent

A running task#

Between discrete steps the fill glides rather than snapping, so a jump from 24% to 36% reads as movement. Start the upload to watch it climb and land on tone="success".

Uploading assets

Indeterminate#

Pass value={null} when there is no percentage to report. The fill becomes a short pill sweeping the track, which says "working" without inventing a number, and the readout falls back to a dash. Radix drops the value from the accessibility tree too, so the bar is announced as indeterminate rather than as zero.

Connecting to the registry

Reading progress#

useScrollProgress() returns how far the document has been scrolled, 0 to 100. Feed it to a chrome bar and you get the reading indicator that rides under a navbar: no track, square ends, and transition="none" so the fill is welded to the scrollbar. A transition here would trail the page by 300ms and read as lag, not polish. Pass it a ref to follow a scroll container instead (a reader pane, a modal body), which is what the panel below does. The blog post page ships the document version.

Constraints feel limiting for about a week. After that they are the reason a new component takes an afternoon instead of a sprint, because most of the decisions are already made, and the ones that remain are the interesting ones.

Constraints feel limiting for about a week. After that they are the reason a new component takes an afternoon instead of a sprint, because most of the decisions are already made, and the ones that remain are the interesting ones.

Constraints feel limiting for about a week. After that they are the reason a new component takes an afternoon instead of a sprint, because most of the decisions are already made, and the ones that remain are the interesting ones.

Constraints feel limiting for about a week. After that they are the reason a new component takes an afternoon instead of a sprint, because most of the decisions are already made, and the ones that remain are the interesting ones.

Constraints feel limiting for about a week. After that they are the reason a new component takes an afternoon instead of a sprint, because most of the decisions are already made, and the ones that remain are the interesting ones.

Constraints feel limiting for about a week. After that they are the reason a new component takes an afternoon instead of a sprint, because most of the decisions are already made, and the ones that remain are the interesting ones.

Constraints feel limiting for about a week. After that they are the reason a new component takes an afternoon instead of a sprint, because most of the decisions are already made, and the ones that remain are the interesting ones.

Constraints feel limiting for about a week. After that they are the reason a new component takes an afternoon instead of a sprint, because most of the decisions are already made, and the ones that remain are the interesting ones.

FAQ#

So it can be dropped. Every part of a Koala component is omittable and composed rather than switched on with a `showX` flag: `<Progress value={64} />` is the bare bar, and adding `ProgressLabel` or `ProgressValue` as children builds the row above it. That also means you can put anything else in that row - a file count, a cancel button, a tone chip.