---
title: Progress
description: The native progress element, restyled - omit value and the indeterminate state is real.
source: https://adamjurek.com/components/progress
---

## Home


A labelled progress bar built on the native `<progress>` element. Give it a
`value` for a real fraction, or omit it for the indeterminate sweep the
browser draws for "something is happening, no percentage yet."

<!-- ::start:showcase demo="progress" height="380" -->
<!-- ::end:showcase -->

## Why it is built this way

`<progress>` carries its own semantics, so a reader hears the fraction
announced with no ARIA written here. Omitting `value` gets the real
indeterminate state the browser draws, rather than a bar animated to fake
one. `label` has no default, because a bar with nothing to announce alongside
the number is not accessible to fix later.

## What it does not do

It does not fake progress. A bar that fills on a timer to look busy tells a
smaller truth than the indeterminate sweep does, so `value` is for a real
fraction only - there is no "looks like progress" mode.

> [!NOTE] Install commands are not written here
> Anything in `packages/ui/registry.ts` gets its TanStack and shadcn commands
> attached automatically, so there is nothing to keep in sync.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/progress.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/progress.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | content · Loading |
| Files | `progress.tsx` |
| Dependencies | None |
| Tags | loading, form, no-deps |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { Progress } from "@sushindustries/ui";

export function UploadStatus() {
	return <Progress label="Uploading" value={42} />;
}
```

## What you should see

A labelled bar filled to 42%. Omit `value` and the bar switches to the
indeterminate sweep the browser draws natively - useful for "something is
happening, no percentage yet" rather than faking it with a value that keeps
resetting to zero.

## If nothing happens

`label` is required and has no default - a bar with no label has nothing for
a screen reader to announce alongside the number. If the bar shows but looks
like the browser default rather than the site's style, `atoms.css` isn't
imported.


## Guides


## Composing it

`Progress` renders its own `<label className="field">` - don't wrap it in
another one. Nesting labels is invalid HTML, and the browser resolves a click
on the outer label to whichever form control it finds first, not necessarily
this one.

## When not to use it

For a percentage-like number with no real completion signal - a bar that
fills on a timer just to look busy is worse than the indeterminate sweep,
which tells the truth about what's known. Reach for `value` only when there's
a real fraction behind it.


## API


<!-- generated:api -->

## Props

| Prop | Type | Default | Does |
| --- | --- | --- | --- |
| `value?` | `number` | - | 0 to `max`. Omit for the indeterminate sweep. |
| `max?` | `number` | `100` | A full bar. The default means `value` can be a percentage with no conversion. |
| `label` | `string` | - | What is progressing. Announced with the number. |

<!-- /generated:api -->

## Notes

`max` only matters relative to `value` - it changes what fraction the bar
fills, not what's displayed as text. There's no built-in "42 of 50" label;
`label` is announced alongside the native percentage a screen reader computes
from `value` and `max` on its own.

Omitting `value` is the only way to get the indeterminate state - passing
`0` still renders a real, empty bar, and reads differently to assistive tech
than "not yet known."


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="progress" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

```tsx
import { Progress } from "@sushindustries/ui";

export function UploadCard({ percent }: { percent: number | undefined }) {
	return (
		<div className="card p-6">
			<h3 className="h4 m-0">report.pdf</h3>
			<div className="mt-4">
				<Progress label="Uploading" value={percent} />
			</div>
		</div>
	);
}
```
