---
title: Progress
description: Displays task completion progress with accessible progressbar semantics.
order: 14
tags: [primitives]
---

# Progress

A progress indicator that supports determinate, indeterminate, and complete states. Manages ARIA attributes, formats values via `Intl.NumberFormat`, and synchronizes state across all slot elements.

```ts
import { Progress } from "@areia/slots";

document.querySelector("#root")!.innerHTML = `
  <div data-slot="progress" data-value="60" class="w-72">
    <span data-slot="progress-label" class="mb-1 block font-bold">Uploading files</span>
    <div data-slot="progress-track" class="win95-inset h-5 bg-white">
      <div data-slot="progress-indicator" class="h-full bg-blue-900"></div>
    </div>
    <span data-slot="progress-value" class="mt-1 block text-xs"></span>
  </div>
`;

const root = document.querySelector('[data-slot="progress"]')!;
const controller = Progress.createProgress(root, {
  value: 60,
  min: 0,
  max: 100,
});
```

## Import

```ts
import { Progress } from "@areia/slots";
```

## Usage

Call `createProgress` with the root element and optional configuration. The primitive reads slot attributes from the DOM, computes percentage and status, and synchronizes ARIA attributes across all parts.

```ts
import { Progress } from "@areia/slots";

const root = document.querySelector<HTMLElement>(
  '[data-slot="progress"]',
)!;
if (!root) throw new Error("No root element found");

const controller = Progress.createProgress(root, {
  value: 75,
  min: 0,
  max: 100,
});

// Programmatic control
controller.setValue(50);
// controller.set({ value: 25, max: 200 });
// controller.destroy();
```

### Auto-bind

Use the `create` helper to discover and bind every unattached root in a scope.

```ts
import { Progress } from "@areia/slots";

const controllers = Progress.create(document);
// controllers.forEach((c) => { ... });
```

## Expected Markup

The primitive expects the following slot structure. Only the root `progress` slot is required; all sub-slots are optional.

```html
<div data-slot="progress" data-value="60">
  <span data-slot="progress-label">Uploading files</span>

  <div data-slot="progress-track">
    <div data-slot="progress-indicator"></div>
  </div>

  <span data-slot="progress-value"></span>
</div>
```

### Indeterminate Progress

Set `value` to `null` (or omit it) to render indeterminate progress:

```html
<div data-slot="progress">
  <span data-slot="progress-label">Loading…</span>

  <div data-slot="progress-track">
    <div data-slot="progress-indicator"></div>
  </div>
</div>
```

### Data Slots

| Slot                 | Required | Description                                                             |
| -------------------- | -------- | ----------------------------------------------------------------------- |
| `progress`           | Yes      | Root element. Receives `role="progressbar"` and ARIA attrs.             |
| `progress-label`     | No       | Accessible label linked via `aria-labelledby`.                          |
| `progress-track`     | No       | Visual track container.                                                 |
| `progress-indicator` | No       | Visual fill bar. Width is set to `{percent}%` for determinate progress. |
| `progress-value`     | No       | Text element that displays the formatted value.                         |

### Generated Attributes

The controller manages these attributes on the root and all sub-slot elements:

| Attribute                  | Element(s)      | Values                                             | Description                                             |
| -------------------------- | --------------- | -------------------------------------------------- | ------------------------------------------------------- |
| `data-state`               | Root, all parts | `"indeterminate"` / `"progressing"` / `"complete"` | Current progress status.                                |
| `data-indeterminate`       | Root, all parts | (present)                                          | Present when there is no value.                         |
| `data-progressing`         | Root, all parts | (present)                                          | Present when progress is underway.                      |
| `data-complete`            | Root, all parts | (present)                                          | Present when value has reached the maximum.             |
| `data-value`               | Root            | `string`                                           | Current numeric value.                                  |
| `data-min`                 | Root            | `string`                                           | Minimum value.                                          |
| `data-max`                 | Root            | `string`                                           | Maximum value.                                          |
| `data-percent`             | Root, indicator | `string`                                           | Computed percentage (0–100). Absent when indeterminate. |
| `role`                     | Root            | `"progressbar"`                                    | ARIA progressbar role.                                  |
| `aria-valuemin`            | Root            | `string`                                           | Minimum value.                                          |
| `aria-valuemax`            | Root            | `string`                                           | Maximum value.                                          |
| `aria-valuenow`            | Root            | `string`                                           | Current value. Removed when indeterminate.              |
| `aria-valuetext`           | Root            | `string`                                           | Human-readable value text.                              |
| `aria-labelledby`          | Root            | ID of label                                        | Set when a `progress-label` slot is present.            |
| `aria-hidden`              | Track, value    | `"true"`                                           | Present on decorative track and value elements.         |
| `style.width`              | Indicator       | `{percent}%`                                       | Inline width set for determinate progress.              |
| `style.inset-inline-start` | Indicator       | `0px`                                              | Inline start set for determinate progress.              |
| `style.height`             | Indicator       | `inherit`                                          | Inline height set for determinate progress.             |

## Events

All events are dispatched from the root element (`data-slot="progress"`).

### Outbound

| Event                   | Detail                      | Description                   |
| ----------------------- | --------------------------- | ----------------------------- |
| `progress:value-change` | `ProgressValueChangeDetail` | Fires when the value changes. |

#### `progress:value-change`

```ts
interface ProgressValueChangeDetail {
  value: number | null;
  previousValue: number | null;
  percent: number | null;
  status: "indeterminate" | "progressing" | "complete";
}
```

| Field           | Description                                       |
| --------------- | ------------------------------------------------- |
| `value`         | The new value (or `null` if indeterminate).       |
| `previousValue` | The previous value before the change.             |
| `percent`       | Computed percentage (or `null` if indeterminate). |
| `status`        | The resolved progress status.                     |

### Inbound

| Event          | Detail                                | Description                                |
| -------------- | ------------------------------------- | ------------------------------------------ |
| `progress:set` | `number \| null \| ProgressSetDetail` | Set the value from outside the controller. |

Dispatch `progress:set` with a number (or `null` for indeterminate) to update the value:

```ts
const root = document.querySelector<HTMLElement>(
  '[data-slot="progress"]',
)!;

// Set a value directly
root?.dispatchEvent(
  new CustomEvent("progress:set", {
    bubbles: true,
    detail: 80,
  }),
);

// Or pass a detail object to set value, min, and max
root?.dispatchEvent(
  new CustomEvent("progress:set", {
    bubbles: true,
    detail: { value: 50, min: 0, max: 200 },
  }),
);
```

## API Reference

### Options

`Progress.createProgress(root, options)`

| Option             | Type                                                                | Default           | Description                                                       |
| ------------------ | ------------------------------------------------------------------- | ----------------- | ----------------------------------------------------------------- |
| `value`            | `number \| null`                                                    | `null`            | Current value. `null` or `undefined` means indeterminate.         |
| `min`              | `number`                                                            | `0`               | Minimum value. If `max < min`, they are swapped automatically.    |
| `max`              | `number`                                                            | `100`             | Maximum value.                                                    |
| `locale`           | `Intl.LocalesArgument`                                              | —                 | Locale used by `Intl.NumberFormat` when rendering the value text. |
| `format`           | `Intl.NumberFormatOptions`                                          | —                 | Number formatting options for the value slot.                     |
| `getAriaValueText` | `(formattedValue: string \| null, value: number \| null) => string` | Default formatter | Custom function to produce the `aria-valuetext` string.           |
| `onValueChange`    | `(value: number \| null) => void`                                   | —                 | Called when the value changes.                                    |

Options can also be set via `data-*` attributes on the root element (e.g. `data-value="60"`, `data-min="0"`, `data-locale="en-US"`). JS options take precedence over data attributes.

### Controller

| Member            | Type                                                      | Description                                              |
| ----------------- | --------------------------------------------------------- | -------------------------------------------------------- |
| `value`           | `number \| null` (getter)                                 | Current value.                                           |
| `min`             | `number` (getter)                                         | Current minimum value.                                   |
| `max`             | `number` (getter)                                         | Current maximum value.                                   |
| `status`          | `"indeterminate" \| "progressing" \| "complete"` (getter) | Current progress status.                                 |
| `percent`         | `number \| null` (getter)                                 | Computed percentage (0–100), or `null` if indeterminate. |
| `setValue(value)` | `(value: number \| null) => void`                         | Set the value.                                           |
| `set(detail)`     | `(detail: ProgressSetDetail) => void`                     | Set value, min, and/or max in one call.                  |
| `destroy()`       | `() => void`                                              | Remove all event listeners and clean up.                 |

#### `ProgressSetDetail`

```ts
interface ProgressSetDetail {
  value?: number | null;
  min?: number;
  max?: number;
}
```

## Controller

The controller is the imperative handle returned by `createProgress`. Use it for programmatic control when you need to read or update progress from application logic.

```ts
import { Progress } from "@areia/slots";

const root = document.querySelector<HTMLElement>(
  '[data-slot="progress"]',
)!;
if (!root) throw new Error("No root element");

const ctrl = Progress.createProgress(root, {
  value: 0,
  min: 0,
  max: 100,
});

// Read state
console.log(ctrl.value); // 0
console.log(ctrl.percent); // 0
console.log(ctrl.status); // "progressing"

// Update value
ctrl.setValue(50);
console.log(ctrl.percent); // 50

// Update multiple properties at once
ctrl.set({ value: 100, min: 0, max: 200 });
console.log(ctrl.status); // "complete"

// Switch to indeterminate
ctrl.setValue(null);
console.log(ctrl.status); // "indeterminate"

// Tear down all listeners and clean up
ctrl.destroy();
```

### Behavior

- **Status derivation**: `indeterminate` when `value` is `null`; `complete` when `value >= max`; `progressing` otherwise. Status attributes are applied to the root and every slot element for consistent CSS styling.
- **Value clamping**: The value is clamped between `min` and `max`. If `max < min`, they are swapped.
- **Percentage computation**: `((value - min) / (max - min)) * 100`, clamped to 0–100. Returns `null` when indeterminate.
- **Indicator sizing**: For determinate progress, the indicator receives `style.width = "{percent}%"`, `style.insetInlineStart = "0px"`, and `style.height = "inherit"`. These are removed when indeterminate.
- **Value formatting**: The `progress-value` slot is populated with the result of `Intl.NumberFormat(locale, format).format(value)`. Set `locale` and `format` options to customize the rendered text.
- **ARIA**: The root receives `role="progressbar"` with `aria-valuemin`, `aria-valuemax`, `aria-valuenow` (removed when indeterminate), and `aria-valuetext`. The label slot is linked via `aria-labelledby`.
- **Deduplication**: Calling `Progress.createProgress()` more than once on the same root returns the existing controller. Destroy it first if you need to rebind with new options.
- **Auto-bind**: The `create()` function discovers all `[data-slot="progress"]` elements in a scope and binds any that are not already bound.
