Skip to content

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.

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

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.

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.

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.

<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:

<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

SlotRequiredDescription
progressYesRoot element. Receives role="progressbar" and ARIA attrs.
progress-labelNoAccessible label linked via aria-labelledby.
progress-trackNoVisual track container.
progress-indicatorNoVisual fill bar. Width is set to {percent}% for determinate progress.
progress-valueNoText element that displays the formatted value.

Generated Attributes

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

AttributeElement(s)ValuesDescription
data-stateRoot, all parts"indeterminate" / "progressing" / "complete"Current progress status.
data-indeterminateRoot, all parts(present)Present when there is no value.
data-progressingRoot, all parts(present)Present when progress is underway.
data-completeRoot, all parts(present)Present when value has reached the maximum.
data-valueRootstringCurrent numeric value.
data-minRootstringMinimum value.
data-maxRootstringMaximum value.
data-percentRoot, indicatorstringComputed percentage (0–100). Absent when indeterminate.
roleRoot"progressbar"ARIA progressbar role.
aria-valueminRootstringMinimum value.
aria-valuemaxRootstringMaximum value.
aria-valuenowRootstringCurrent value. Removed when indeterminate.
aria-valuetextRootstringHuman-readable value text.
aria-labelledbyRootID of labelSet when a progress-label slot is present.
aria-hiddenTrack, value"true"Present on decorative track and value elements.
style.widthIndicator{percent}%Inline width set for determinate progress.
style.inset-inline-startIndicator0pxInline start set for determinate progress.
style.heightIndicatorinheritInline height set for determinate progress.

Events

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

Outbound

EventDetailDescription
progress:value-changeProgressValueChangeDetailFires when the value changes.

progress:value-change

interface ProgressValueChangeDetail {
  value: number | null;
  previousValue: number | null;
  percent: number | null;
  status: "indeterminate" | "progressing" | "complete";
}
FieldDescription
valueThe new value (or null if indeterminate).
previousValueThe previous value before the change.
percentComputed percentage (or null if indeterminate).
statusThe resolved progress status.

Inbound

EventDetailDescription
progress:setnumber | null | ProgressSetDetailSet the value from outside the controller.

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

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)

OptionTypeDefaultDescription
valuenumber | nullnullCurrent value. null or undefined means indeterminate.
minnumber0Minimum value. If max < min, they are swapped automatically.
maxnumber100Maximum value.
localeIntl.LocalesArgumentLocale used by Intl.NumberFormat when rendering the value text.
formatIntl.NumberFormatOptionsNumber formatting options for the value slot.
getAriaValueText(formattedValue: string | null, value: number | null) => stringDefault formatterCustom function to produce the aria-valuetext string.
onValueChange(value: number | null) => voidCalled 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

MemberTypeDescription
valuenumber | null (getter)Current value.
minnumber (getter)Current minimum value.
maxnumber (getter)Current maximum value.
status"indeterminate" | "progressing" | "complete" (getter)Current progress status.
percentnumber | null (getter)Computed percentage (0–100), or null if indeterminate.
setValue(value)(value: number | null) => voidSet the value.
set(detail)(detail: ProgressSetDetail) => voidSet value, min, and/or max in one call.
destroy()() => voidRemove all event listeners and clean up.

ProgressSetDetail

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.

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.