Determinate, indeterminate, and the sizes
A value makes the bar say how far; no value makes it say "still going", and the fill travels instead. max sets the scale — a download in bytes needs no arithmetic first.
How far along a task is — or, when nobody can say, that it is under way at all.
@pacit/components/progresspct-progressNo APG pattern applies, and none is invented — the reasoning is under Accessibility ↓
import { Component, signal } from '@angular/core';
import { PctButton } from '@pacit/components/button';
import { PctProgress } from '@pacit/components/progress';
/** With a value it reports the number; without one it honestly says "still working". */
@Component({
selector: 'demo-progress',
imports: [PctButton, PctProgress],
// A width, not only a ceiling: a bar 100% of a container that is as wide as its content
// is a bar as wide as the button beside it (`lesson-160`).
styles: ':host { display: grid; gap: 12px; inline-size: min(24rem, 100%); }',
template: `
<pct-progress [value]="done()" ariaLabel="Uploading" />
<pct-progress ariaLabel="Connecting" />
<div>
<button pctButton variant="ghost" size="sm" (click)="move(-10)">
−10
</button>
<button pctButton variant="ghost" size="sm" (click)="move(10)">
+10
</button>
</div>
`,
})
export class ProgressDemo {
readonly done = signal(62);
move(by: number): void {
this.done.update((at) => Math.min(100, Math.max(0, at + by)));
}
}progress.demo.ts — the same file runs here and is highlighted under Code
One import per component, and the shortest possible use — the package itself is installed once, on the start page.
import { PctProgress } from '@pacit/components/progress';| Exported | What it is |
|---|---|
PctProgresscomponentpct-progress | A progress bar: how far along a task is, or that it is under way at all. |
<pct-progress [value]="42" ariaLabel="Uploading" />Each example is the component itself, not a screenshot. Open the code under any of them.
A value makes the bar say how far; no value makes it say "still going", and the fill travels instead. max sets the scale — a download in bytes needs no arithmetic first.
Read from the source at build time — the types, the defaults and the lines of documentation are the library's own.
A progress bar: how far along a task is, or that it is under way at all.
| Name | Type | What it does |
|---|---|---|
valuedefault null | number | null | How far along, in the same unit as max. null — the default — is indeterminate: the task is under way and how far is not known. |
maxdefault DEFAULT_MAX | number | The value that means "done" — 100 by default, so a bare [value]="40" is forty per cent. |
ariaLabeldefault '' | string | The accessible name — what is progressing. There is no default: see the class note. |
ariaLabelledbydefault '' | string | The id of the element that already names this bar — a caption or a heading standing beside it. The usual case: the sentence a sighted user reads is the name a screen-reader user should hear, and repeating it in ariaLabel is two strings to keep in step. |
sizedefault config.defaultSize | PctSize | Size; taken from the global configuration by default (req-api-config). |
tonedefault null | PctTone | null | What the work IS, drawn as a mark beside the bar and as the colour of the fill — not how far it got, which is value. A failed upload is tone: 'danger' at whatever percentage it stopped at; a finished one is 'success' at 100. An indeterminate bar may carry a tone too: "retrying, and the last attempt failed" is a real state, and nothing here pretends to know what the work will do next. |
(click), (focus), the form's submit — are the platform's, and two-way models above carry the rest. The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.
| Attribute | Bound to | Note |
|---|---|---|
class | pct-progress | |
data-pct-size | size() | |
data-pct-indeterminate | value() === null ? "" : null | The state a skin reads, and the one the stylesheet's band rule turns on. It is written where the paint is — the ARIA half of the same fact is the <progress>'s own, and this component writes no ARIA anywhere (0039). |
data-pct-tone | tone() |
Two ways in, both stable. No class names to depend on.
In a test, PctProgressHarness from @pacit/components/testing reads a part by the name below and a state by its attribute — the harness gate holds the names to the package.
| Part | Selector | What it is |
|---|---|---|
bar | [data-pct-part="bar"] | the box that clips — the groove and the fill live inside it |
fill | [data-pct-part="fill"] | the part that is done |
icon | [data-pct-part="icon"] | the tone's mark, drawn only when the bar carries a tone |
track | [data-pct-part="track"] | the full length |
Every value the component reads, with the light and the dark default. Override a token at any scope and it cascades like any custom property.
| Token | Styles |
|---|---|
--pct-progress-fill-bg{pct.primary}#2563eb · #60a5fa | Background of the fill. |
--pct-progress-fill-bg-danger{pct.danger}#b91c1c · #f87171 | Colour of the filled part of a bar whose work failed. |
--pct-progress-fill-bg-info{pct.info}#2563eb · #60a5fa | Colour of the filled part of a bar reporting something informational. |
--pct-progress-fill-bg-success{pct.success}#15803d · #4ade80 | Colour of the filled part of a bar whose work succeeded. |
--pct-progress-fill-bg-warning{pct.warning}#b45309 · #fbbf24 | Colour of the filled part of a bar whose work needs attention. |
--pct-progress-fill-size33% | Length of the fill while indeterminate, as a share of the track. |
--pct-progress-icon-gap{pct.space.2}4px | Space between the tone's mark and the bar. |
--pct-progress-icon-size16px | Size of the box the tone's mark is drawn in, beside the bar. |
--pct-progress-track-bg{pct.border}#e2e8f0 · #334155 | Background of the track. |
--pct-progress-track-height8px | Height of the track. |
--pct-progress-track-height-lg12px | Height of the track at size lg. |
--pct-progress-track-height-sm4px | Height of the track at size sm. |
--pct-progress-track-radius999px | Corner radius of the track. |
A brand override in a few declarations. Scope it to a theme attribute, a page or one container — the component never notices.
[data-theme='brand'] {
--pct-progress-fill-bg: #0f766e;
--pct-progress-track-bg: #ccfbf1;
} Pattern: none — there is no APG pattern for a progress bar. What there is is the progressbar role, and this component does not write it: the bar is a <progress>, so the role, the bounds, the value and the indeterminate state are the element's. Named in the class JSDoc.
| key | effect | test |
|---|
Empty by design, and for a stronger reason than usual: a progress bar is not a control. Nothing is pressed, nothing is dragged, and a <progress> takes focus in no engine — measured in apps/sandbox-e2e/src/progress.spec.ts, which asks the element to focus itself and asserts that it does not, then walks Tab past it (req-api-platform).
Every line is a test in three engines or a gate on every commit. The gaps are listed the same way, each with the requirement that will close it.
libs/components/progress/src/progress.tsapps/sandbox-e2e/src/progress.spec.ts — the empty map is the claim, so what is gated is that the element refuses focus in three engines and that Tab steps from one button beside the bar to the nextapps/sandbox-e2e/src/a11y.spec.ts — /progress in SBX_ROUTES, so the WCAG 2.2 AA sweep takes it. aria-progressbar-name is the rule that bites here, and every bar in the view is namedapps/sandbox-e2e/src/visual.spec.ts — progress-sizes and progress-sizes-rtl, the three thicknesses with the fill starting from the edge the writing direction gives it. The INDETERMINATE bar is in no baseline, and that is the one gap the clip above leaves open: overflow clips the painting and not the box, so the band's getBoundingClientRect leaves the groove after the fix exactly as it did before it — no assertion over the DOM, in jsdom or in a browser, can see the difference. Only a picture canforced-colors: active — no state carried by colour alonelibs/components/progress/src/progress.scss — the fill and the band are Highlight, the groove Field, and its extent a CanvasText outline: an outline and not a border because the fill is positioned over the groove, and on the CLIPPING BOX (bar, the host's job until tones needed a mark beside the groove) and not on the <progress>, because that box clips the band and a clip erases a descendant's outline (measured — the two boxes are one rectangle, so the ring lands where it always did). The reading in the sheet: libs/components/progress/src/progress.spec.ts. In a browser: apps/sandbox-e2e/src/forced-colors.spec.ts, on the bar part with the width and the style read beside the colour — until 2026-09-07 it stood on track, where outline-color with no outline computes to currentColor, which in this mode is CanvasText, so it passed on the fallback and asked nothing. The mechanism this refuses is a gradient band, dropped outright in that mode. The tone is measured here too: the fill's four toned colours go the way every author colour goes in that mode, and what is left is the mark — which is the whole reason a tone is never only a colourprefers-reduced-motion — duration from a tokenapps/sandbox-e2e/src/preferences.spec.ts — the band reads 1500 ms (the loop token slows, it does not stop: a still band would stop saying anything is under way) and the determinate fill's transition drops below 1 ms. Neither is a media query in the component's sheetreq-a11y-touch sets has nothing to apply to--pct-control-height-*deliberately not — the axis is --pct-progress-track-height (4 / 8 / 12 px). A bar is not a control and lining its box up with a field's would say it were one; libs/tokens/src/component.progress.json says so in a comment. The reading: apps/sandbox-e2e/src/progress.spec.ts, three thicknesses ascending with the widths unchangeddir="rtl" screenshotlibs/components/progress/src/progress.scss — the fill sits on inset-inline-start and the band's keyframes animate the same property. The screenshot: progress-sizes-rtl; the geometry: apps/sandbox-e2e/src/rtl.spec.ts; and the band's DIRECTION reverses in progress.spec.ts, sampled over six frames in both writing directionsNG05xxapps/sandbox-e2e/src/hydration.spec.ts — /progress in SBX_ROUTESFormValueControl. What it reports is the application's own statelibs/components/parts.snapshot.md, tools/check-parts.mjs — the Parts row abovecontrast.policy.jsonlibs/tokens/src/contrast.policy.json — three entries (fill against the groove, fill against the page, and the groove against the page as a warn, the slider's rail one component over); libs/tokens/tokens.snapshot.mdPCT_TEXTSnot applicable — the component draws no text. The name is the consumer's, through ariaLabel / ariaLabelledby, and there is no default to translatelibs/components/size.snapshot.md — the ./progress rowdocs/acr/at/ — three readers, whole sandbox, and the instrument cannot reach this component: a progressbar is not focusable, so a Tab walk never lands on one. What the logs hold is the buttons around it (+10 · button., plus 10, button), which say nothing about the bar. Whether the value and its text are announced needs a reader asked to READ the page rather than walk it — a different pass, now precisely namedapps/sandbox/src/app/views/progress/ (the sandbox view). The published site: /components/progress — prerendered, the demo's own source is the code tablibs/components/progress/src/progress.spec.ts — the four names each draw their own path and each writes its own state attribute, and an untoned bar draws no mark and carries no attribute; apps/sandbox-e2e/src/tones.spec.ts — the mark is in the box in three engines, the fill takes the toned colour, and both survive forced-colors: active as CanvasText. tools/check-icons.mjs holds the names in both directions: a tone with no drawing is a promise nobody keepslibs/components/progress/src/progress.spec.ts — 34 cases, the last three over the sheet itself (the band's travel and the clip that answers it, and the box the forced-colors ring is drawn on) read from the sheet's own rules, since jsdom matches no forced-colors query and computes no layout; libs/components/mutation.snapshot.md — progress.ts at 92.31, zero clock-kills, and the four survivors equivalent: isDevMode() forced true, value === undefined dropped (Number(undefined) is NaN and fails isFinite anyway) and the two that drop typeof value === 'number' (Number(n) of a number is that number) — each proved by applying it and watching the cases stay greenRead at build time from the tracked snapshots, not typed here.
The whole component is one boundary drawn twice (0049): the platform's element for what a reader hears, our own drawing for what an eye sees. It is the checkbox's shape one component over, and it is here for a measured reason rather than a stylistic one — appearance: none is the price of painting a <progress>, and it takes the engine's indeterminate animation away.
pct-button for a button that is working. A standalone one is a tag of its own when somebody needs it.pct-icon is aria-hidden, so a screen-reader user hears the <progress> and the consumer's own sentence beside it and nothing about success or failure. If a tone has to be heard, it belongs in that sentence — which is what ariaLabelledby points at — and not in a string this component invented (0039). Until 2026-09-07 this row said there were no tones at all, and the reason it gave was the right one: the second channel is an icon, four names into PctIconName at once, and a set no single component could judge. Two components wanted the same four, and the set is 0076's.ariaLabel nor ariaLabelledby gets a dev-mode warning and an axe violation on their own page; this repository's static gate can only see its own templates.