# Progress bar

A progress bar drawn in eighth-block glyphs, exact to an eighth of a cell, with 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 asciicn agent skill, for building any ASCII effect on the engine, is at [/.well-known/agent-skills/asciicn/SKILL.md](/.well-known/agent-skills/asciicn/SKILL.md).

> **This is an example, not the only option.** It is one component built on the asciicn 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 `AsciiShader` ([Write your own](/docs/engine/write-your-own.md)), or draw a CPU grid with `AsciiGrid`, and read this example's source for technique. Its full source is in [https://asciicn.dev/r/progress-bar.json](https://asciicn.dev/r/progress-bar.json), under `files[].content`.

## Installation [#installation]

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

## Usage [#usage]

```tsx
import { ProgressBar } from "@/components/asciicn/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 is a row of full blocks that ends in one of the eighth blocks `▏▎▍▌▋▊▉`, so 37.5% of a 32-cell bar is exactly 12 cells: `████████████`. The bar is as many whole cells as fit its container, measured in the glyph font, and it measures again when the container or the zoom changes.

Behind the fill runs a track of `░` (`track="shade"`), `·`, `─` or nothing. A new value slides in eighth by eighth over 600ms.

Leave `value` out when you can't measure progress. A comet of `░▒▓█` sweeps the track instead:

```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 of `█` swatches underneath. Boundaries between parts snap to whole cells, because a cell holds one glyph in one colour. The leading edge of the total keeps its eighth-cell precision.

```tsx title="examples/progress-bar-usage.tsx"
import { ProgressBar } from "@/components/asciicn/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-lg border p-5 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"
      />
      <ProgressBar
        label="Bandwidth"
        value={312}
        max={1000}
        valueText="312 of 1,000 GB"
        size="sm"
        track="dots"
      />
      <ProgressBar
        label="Build minutes"
        value={5770}
        max={6000}
        valueText="5,770 of 6,000 min"
        size="sm"
        track="dots"
        color="red"
      />
    </div>
  );
}
```

### Upload list [#upload-list]

Three states at `size="lg"`: a finished file in green, one at 46.25%, and an indeterminate step on a `─` track while the server transcodes.

```tsx title="examples/progress-bar-uploads.tsx"
import { ProgressBar } from "@/components/asciicn/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"
        />
      </li>
      <li>
        <ProgressBar
          label="product-tour.mp4"
          value={46.25}
          size="lg"
          color="purple"
        />
      </li>
      <li>
        <ProgressBar
          label="Transcoding product-tour.mp4"
          size="lg"
          color="purple"
          track="line"
        />
      </li>
    </ul>
  );
}
```

### Onboarding steps [#onboarding-steps]

`segments` cuts the bar into blocks with a blank cell between them. The value fills the blocks in order, so three of five tasks fill three blocks.

```tsx title="examples/progress-bar-steps.tsx"
import { ProgressBar } from "@/components/asciicn/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-lg border p-5">
      <ProgressBar
        label="Getting started"
        value={done}
        max={TASKS.length}
        valueText={`${done} of ${TASKS.length}`}
        segments={TASKS.length}
        color="orange"
        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
                  ? "text-ascii-orange font-mono"
                  : "text-muted-foreground font-mono"
              }
            >
              {task.done ? "■" : "□"}
            </span>
            <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?: AsciiColor }, 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"` | Glyph cell height: 12, 16 or 20px. |
| `track` | `"shade" \| "dots" \| "line" \| "none"` | `"shade"` | Glyph along the empty track: ░, ·, ─ or nothing. |
| `color` | `AsciiColor` | `"ink"` | Colour of the fill, a palette name or any CSS colour. |
| `segments` | `number` |  | Cut the bar into this many blocks a cell apart, for step meters. |
| `columns` | `number` | `As many as fit` | Fix the bar's width in cells instead of filling the container. |
| `animate` | `boolean` | `true` | Ease between values and grow in on mount. |
| `cell` | `number` |  | Glyph cell height in CSS pixels. Overrides size. |

## Notes [#notes]

* The root has `role="progressbar"`, is named by the label and reports `aria-valuenow` and `aria-valuetext`. An indeterminate bar leaves the value out, as assistive technology expects. The glyphs are hidden from screen readers.
* The bar is DOM text in `--ascii-font`, one line tall, so it stays sharp at any zoom and its height never changes. Before the first measurement it draws 32 cells.
* With reduced motion, values jump instead of easing and the comet rests in the middle of the track.
* The comet redraws at 30fps and pauses when the bar is offscreen or the tab is hidden.