# Progress bar

A progress bar built from dither cells: any fill texture with a solid leading edge, stacked parts, step segments and an indeterminate comet.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. The dithercn agent skill, for building any dither on the engine, is at [/.well-known/agent-skills/dithercn/SKILL.md](/.well-known/agent-skills/dithercn/SKILL.md).

> **This is an example, not the only option.** It is one component built on the dithercn engine. Install it when it is what you were asked for. For any other look, don't stretch its props to fit: write your own field on `DitherShader` ([Write your own dither](/docs/engine/write-your-own.md)), and read this example's source for technique. Its full source is in [https://dithercn.dev/r/progress-bar.json](https://dithercn.dev/r/progress-bar.json), under `files[].content`.

## Installation [#installation]

```bash
npx shadcn@latest add https://dithercn.dev/r/progress-bar.json
```

## Usage [#usage]

```tsx
import { ProgressBar } from "@/components/dithercn/progress-bar";

export function Upload({ percent }: { percent: number }) {
  return <ProgressBar label="Uploading report.pdf" value={percent} />;
}
```

`value` runs from 0 to `max`, which is 100 unless you set it. The fill sits on a faint track in the same colour. With `fill="gradient"` it is solid at the leading edge and thins out behind it; `dotted`, `hatched` and `solid` fill evenly. Every texture ends in a solid one-cell cap, so the exact value is always readable. The fill grows in on mount and eases between values.

Leave `value` out when you can't measure progress. The bar becomes indeterminate: a comet with a dense head and a dithered tail sweeps across the track.

```tsx
<ProgressBar label="Indexing repository" />
```

## Examples [#examples]

### Usage card [#usage-card]

`parts` stacks several values in one bar, each in its own colour, with a legend underneath. Here storage is split by file type. Build minutes are close to the limit, so that bar uses `color="red"`, `fill="hatched"` and a low bloom.

```tsx title="examples/progress-bar-usage.tsx"
import { ProgressBar } from "@/components/dithercn/progress-bar";

const STORAGE = [
  { color: "blue", label: "Images", value: 4.1 },
  { color: "purple", label: "Video", value: 2.3 },
  { color: "green", label: "Backups", value: 0.8 },
] as const;

export default function ProgressBarUsage() {
  return (
    <div className="bg-background border-border flex w-full max-w-md flex-col gap-6 rounded-2xl border p-5 [--dither-paper:var(--background)] sm:p-6">
      <div className="flex flex-col gap-1">
        <span className="font-medium">Usage this cycle</span>
        <span className="text-muted-foreground text-sm">
          Resets on November 1
        </span>
      </div>
      <ProgressBar
        label="Storage"
        parts={STORAGE}
        max={10}
        unit=" GB"
        valueText="7.2 of 10 GB"
        fill="solid"
      />
      <ProgressBar
        label="Bandwidth"
        value={312}
        max={1000}
        valueText="312 of 1,000 GB"
        size="sm"
      />
      <ProgressBar
        label="Build minutes"
        value={5770}
        max={6000}
        valueText="5,770 of 6,000 min"
        size="sm"
        color="red"
        fill="hatched"
        bloom="low"
      />
    </div>
  );
}
```

### Upload list [#upload-list]

Three states at `size="lg"`: a finished file in solid green, one in progress, and an indeterminate step while the server transcodes.

```tsx title="examples/progress-bar-uploads.tsx"
import { ProgressBar } from "@/components/dithercn/progress-bar";

export default function ProgressBarUploads() {
  return (
    <ul className="flex w-full max-w-md flex-col gap-5">
      <li>
        <ProgressBar
          label="hero-poster.png"
          value={100}
          valueText="Done"
          size="lg"
          color="green"
          fill="solid"
        />
      </li>
      <li>
        <ProgressBar
          label="product-tour.mp4"
          value={46}
          size="lg"
          color="purple"
          bloom="low"
        />
      </li>
      <li>
        <ProgressBar
          label="Transcoding product-tour.mp4"
          size="lg"
          color="purple"
        />
      </li>
    </ul>
  );
}
```

### Onboarding steps [#onboarding-steps]

`segments` cuts the bar into blocks a cell apart. With one block per task, three of five tasks fill three blocks.

```tsx title="examples/progress-bar-steps.tsx"
import { ProgressBar } from "@/components/dithercn/progress-bar";

const TASKS = [
  { done: true, label: "Create a project" },
  { done: true, label: "Connect a repository" },
  { done: true, label: "Add a custom domain" },
  { done: false, label: "Invite your team" },
  { done: false, label: "Turn on alerts" },
] as const;

const done = TASKS.filter((task) => task.done).length;

export default function ProgressBarSteps() {
  return (
    <div className="bg-background border-border flex w-full max-w-sm flex-col gap-5 rounded-2xl border p-5 [--dither-paper:var(--background)]">
      <ProgressBar
        label="Getting started"
        value={done}
        max={TASKS.length}
        valueText={`${done} of ${TASKS.length}`}
        segments={TASKS.length}
        color="orange"
        fill="dotted"
        size="lg"
      />
      <ul className="flex flex-col gap-2 text-sm">
        {TASKS.map((task) => (
          <li key={task.label} className="flex items-center gap-2.5">
            <span
              aria-hidden="true"
              className={
                task.done
                  ? "bg-dither-orange size-2 rounded-[2px]"
                  : "border-border size-2 rounded-[2px] border"
              }
            />
            <span
              className={
                task.done
                  ? "text-muted-foreground line-through"
                  : "text-foreground"
              }
            >
              {task.label}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}
```

## Props [#props]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` (required) | `string` |  | What is in progress. Shown above the bar and used as its accessible name. |
| `value` | `number \| null` |  | Progress from 0 to max. Leave it out for an indeterminate bar. |
| `parts` | `ProgressPart[]` |  | { label: string; value: number; color?: DitherColor; fill?: FillVariant }, stacked left to right instead of value. |
| `unit` | `string` |  | With parts: shown after each value in the legend, such as " GB". |
| `max` | `number` | `100` | The value that means done, or the capacity for parts. |
| `valueText` | `string` | `The percentage` | Text for the readout and for screen readers. |
| `hideLabel` | `boolean` | `false` | Hide the label and readout visually. Screen readers still get them. |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Bar height: 2, 4 or 6 cells. |
| `fill` | `"gradient" \| "dotted" \| "hatched" \| "solid"` | `"gradient"` | Texture of the fill. Gradient is densest at the leading edge. |
| `color` | `DitherColor` | `"ink"` | Colour of the fill and its track, a palette name or any CSS colour. |
| `bloom` | `"off" \| "low" \| "high"` | `"off"` | Soft glow in the fill's colour. |
| `segments` | `number` |  | Cut the bar into this many blocks a cell apart, for step meters. |
| `animate` | `boolean` | `true` | Grow in on mount and ease between values. |
| `cell` | `number` | `--dither-cell` | Size of one dither cell in CSS pixels. |

## Notes [#notes]

* The root has `role="progressbar"`, is named by the label and reports the value (or `valueText`). An indeterminate bar leaves out the value, as assistive technology expects.
* Width changes ease over 600ms; the comet moves only by transform. With reduced motion the fill jumps to its value and the comet rests in the middle of the track.
* It is plain HTML with CSS masks: no SVG, canvas or animation loop.