registry:ui · NativeHTML
progress — the element, painted
W3C-first: progress IS the native <progress> element. Role, value semantics, min/max mapping and the indeterminate state all belong to the browser. The component adds only the jixoai paint — 1px frame, brand fill, a terminal stripe sweep for the indeterminate run — and an optional label with a polite % readout.
$npx jixoai-ui add progress
https://ui.jixoai.com/r/progress.json
overview
Overview
The native element carries the semantics; the family carries two clocks, one seam, and a polite readout — all measured.
The semantics are the platform's, and the family adds nothing to them: a native <progress> exposes the progressbar role and
its value mapping natively (indeterminate simply omits the current value — measured
position −1 with no valuenow), the fallback content inside the element serves ancient
engines, and zero ARIA wiring is authored. The component paints the bar (appearance
reset across both engines, a 1px hairline frame over a muted track, brand fill at a
10px height and a 4px corner seam), and adds a label + live % readout.
There are TWO clocks, and both are live. The INDETERMINATE clock: a terminal-stripe
sweep at var(--motion-indeterminate, 900ms) linear, a 24px tile — measured
0.9s/linear/infinite, slowing to 4s under prefers-reduced-motion. The VALUE clock: the
fill's authored 200ms decelerate is live — burst-measured (clip frames decoded
in-page), the fill edge is mid-tween at ~35ms and settles inside ~150ms of paint, not a
first-frame jump — and the tween PERSISTS under reduced-motion: the css kill wins the
cascade, yet the engine's own smoothing keeps the visible motion (css-unreachable — a
reduced-motion user still sees the fill animate). Instrument note, carried honestly: a
single capture taken ≥200ms after the set sees two settled bars and reads "inert" —
burst capture is the truth instrument for paint transitions. The % readout is role=status: polite — announced when the reader
is idle, never interrupting.
The eight axes are near-zero by ruling (value/max/label are data semantics): density is
managed but the paint is density-invariant (measured 10px at every rung — the height
equation reads the fixed 4px unit); the radius corner rides the own 4px promotion seam;
theme is the frozen-ink seam's third instance — the fill re-derives with the brand
chain while the track, frame and readout inks keep their light literals; size echoes on
the root; shape/color/elevation/motion supply unread. Kinship: spin (the activity twin for spaces a bar cannot
fit) and timeline (the position-on-a-scale
cousin).
progress
A determinate bar driven live from the playground, and an indeterminate one below — same element, no value attribute.
<Progress {value} label="deploy" />W3C foundation
What the platform gives
The element exposes aria-valuenow/min/max natively (indeterminate simply omits valuenow), renders fallback content for ancient engines, and needs no ARIA wiring from us. The paint resets both engines' ::-progress pseudo-elements so WebKit and Gecko draw the same bar.
<script lang="ts">
import Progress from '@ui/progress.svelte';
</script>
<Progress value={0.42} label="sync" /> <!-- 42% -->
<Progress value={done} max={total} /> <!-- your scale -->
<Progress /> <!-- indeterminate: activity, not progress -->types
Types
Two states, one element: determinate maps a 0..max scale; omitted value means indeterminate.
progress · states
<script lang="ts">
import Progress from '@ui/progress.svelte';
</script>
<div class="flex flex-wrap items-start gap-6">
<div class="flex min-w-56 flex-col gap-3 border border-border p-4"><span class="font-nav text-primary text-[11px] uppercase tracking-[0.24em]">determinate</span><Progress value={0.42} label="sync" /><span class="text-muted-foreground text-[12.5px]">value present — a real 0..max position with a % readout</span></div>
<div class="flex min-w-56 flex-col gap-3 border border-border p-4"><span class="font-nav text-primary text-[11px] uppercase tracking-[0.24em]">indeterminate</span><Progress label="connecting" /><span class="text-muted-foreground text-[12.5px]">value omitted — activity, not progress (terminal stripe sweep)</span></div>
</div>usage
Usage
Pass a 0..max value, or omit it for the honest 'something is happening' state.
<script lang="ts">
import Progress from '@ui/progress.svelte';
</script>
<Progress value={0.42} label="sync" /> <!-- 42% -->
<Progress value={done} max={total} /> <!-- your scale -->
<Progress /> <!-- indeterminate: activity, not progress -->theming
Density and tokens
The bar's paint is density-INVARIANT — the height equation reads the fixed 4px unit (measured 10px at every rung, floor included) — and its clocks ride promotion seams: the sweep's 900ms linear and the fill's corner seam are reported constants a consumer var can re-shape.
| Token | Default | Source |
|---|---|---|
| brand fill | var(--primary) | color |
| --progress-radius | 4px | component |
| --motion-indeterminate | 900ms | structural |
| --motion-linear | linear | structural |
| --jx-unit | 0.25rem | density |
api
API
Three data props and the class passthrough. The generated meta carries 12 NAMED entries (value, max, label, the eight axes, class) and no rest key — the component is rest-less: undeclared attributes are dropped, not spread, and the element's role and value semantics are native, not forwarded. The universal fold serves the eight axis rows.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| value | number | — | 0..max; omitted ⇒ indeterminate ("activity", not "progress"). |
| max | number | 1 | The element's own spec default. |
| label | string | — | Visible label above the bar (also names the element via aria-label; without one the bar falls back to the generic name "progress"). |
| class | string | '' | Class passthrough to the root. |
Universal props
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | the base scale — root font-size; parts size in em |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' · ambient scope | corner geometry (CSS corner-shape; §14 degrade table) |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | corner size; auto = the concentric broadcast (§3) |
| density | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | spacing/leading scale over the kernel channels (§4) |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' · ambient scope | the hue axis of the fixed oklch primary system (§5) |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' · ambient scope | light/dark profile; system = the JS-mutable global (§6) |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' · ambient scope | official M3 levels over the surface ladder (§7) |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' · ambient scope | intensity across the motion kernels (§8) |
axes
The eight axes on progress
The zero-hit family, measured honestly: density is managed but the paint is density-INVARIANT (10px at every rung — the height equation reads the fixed unit); the corner rides the own 4px promotion seam (axis unread); theme is the frozen-ink seam's third instance — the brand fill re-derives while track, frame and readout inks stay light literals; size echoes on the root; shape/color/elevation/motion supply unread. Two live clocks, both off the axis: the 0.9s stripe sweep and the fill's burst-measured tween that persists under reduced-motion.
| Property | Type | Default | Description |
|---|---|---|---|
| density | '2xs' | 'xs' | 'sm' | 'default' | 'lg' | 'auto' | number (+ the five legacy spellings) | 'auto' | MANAGED BUT PAINT-INVARIANT — the rung attr rides the root and the channels stay open for composed descendants, but the bar's own paint is density-invariant: the height equation calc(var(--jx-unit) * 2.5) reads the FIXED 4px unit (measured 10px at every rung including the 2xs stamp), and the readout voices ride promoted label steps (12px, measured constant). The zero-hit ruling stands — value/max/label are data semantics. Number unit: coefficient. |
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' | ROOT ECHO — the §11 stamp scales the field root (the readout head rides the root's font), while the native bar keeps its 10px equation. Zero --jx-size-effective readers (grep receipt). Number unit: px. |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' | SUPPLY-ONLY — zero shape-channel readers (grep receipt: no corner-shape or factor consumer in ui/progress/). Number unit: none. |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' | OWN SEAM, AXIS UNREAD — the bar's corner rides the promotion seam var(--progress-radius, 4px) (measured 4px ambient; stamped 9px → 9px live in the probe — a consumer var can re-shape it), and the axis carriers are unread (zero radius-effective readers, grep receipt). Number unit: px. |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' | SUPPLY-ONLY — the fill paints var(--primary) straight from the legacy chain (the global brand-hue runtime moves it), with zero --jx-color-effective readers (grep receipt): the hue AXIS never reaches the bar. Number unit: hue degrees. |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' | THE FROZEN-INK SEAM, THIRD INSTANCE (measured per element under .dark) — the FILL re-derives: the pseudo rule paints var(--primary), a live legacy chain, so the fill flips to the drifted dark primary (measured oklch(0.6489 …) → 0.7044-family). Everything else freezes: the track (--jx-muted atom), the 1px frame (--jx-border atom) and the label/value readout inks (--jx-muted-foreground/--jx-foreground atoms) all keep their LIGHT values under dark — a brand stripe on a light track with light ink. system/auto = tree inheritance. No number lane. |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' | SUPPLY-ONLY — zero elevation-carrier readers (grep receipt); the bar carries a 1px frame, no shadow tier. Number unit: dp. |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' | TWO LIVE CLOCKS, BOTH OFF THE AXIS — the indeterminate sweep rides the promotion seams var(--motion-indeterminate, 900ms) linear infinite (measured 0.9s/linear/24px tile) and slows to 4s under prefers-reduced-motion (measured); the determinate fill's authored 200ms decelerate is LIVE — burst-measured (3 runs, clip frames decoded in-page): the edge moves by 32–53ms and settles by 82–99ms, and the tween PERSISTS under reduced-motion (moved 32–36ms, settled 98–103ms — the engine's smoothing is css-unreachable); a 3s-linear injection stretches the paint to ≈4–4.5s across instruments (3.0s by slope fit; 4.1s task-39; 4.5s on the re-verify's CDP-timestamped capture — the stretch is slope-consistent linear, the exact span is instrument-bound), so the transition channel governs; --jx-motion-effective has zero readers (grep receipt). Number unit: coefficient. |
Receipts: the two live clocks (sweep 0.9s/linear/infinite with a 24px tile, 4s under reduced motion; the fill edge burst-measured moving by 32–53ms and settling by 82–99ms in normal mode — and PERSISTING under reduced motion, 32–36ms moved / 98–103ms settled across 3 runs, with a 3s-linear injection stretching the paint to a slope-consistent linear crawl (≈4–4.5s, instrument-bound); a single capture taken ≥200ms after the set would read "inert" — burst is the truth instrument for paint transitions), the density-invariant height (10px at the 2xs stamp), the radius seam (4px ambient, 9px stamped), the theme split (fill 0.6489-family → 0.7044-family under .dark while the label ink stayed oklch(0.3211 0 0)) and the platform mapping (indeterminate position −1, valuenow omitted; aria-valuenow=100 at max=250) were measured on this page's served DOM (probe, task 34; the fill-clock burst receipts re-measured at task 39); the unread rows carry grep receipts over ui/progress/. The query() seat below rides the md viewport key (48rem) on the size lane.
<script lang="ts">
import Progress from '@ui/progress.svelte';
import { query } from '@lib/universal-props-query.svelte';
</script>
<!-- below 48rem the base (13px) applies; at 48rem+ the md case (18px)
wins — the readout root scales, the bar stays 10px -->
<Progress value={0.6} label="sync" size={query({ md: 18 }, 13)} />progress · query()
Media keys are min-width: below 48rem the base (13px root) applies; at 48rem and wider the md case wins (18px) — the readout root scales while the bar stays 10px. The number lane goes bare. Resize across 48rem.
<script lang="ts">
import Progress from '@ui/progress.svelte';
import { query } from '@lib/universal-props-query.svelte';
</script>
<!-- below 48rem the base (13px) applies; at 48rem+ the md case (18px)
wins — the readout root scales, the bar stays 10px -->
<Progress value={0.6} label="sync" size={query({ md: 18 }, 13)} />Progress · universal props
<Progress size={18} density="small">…</Progress>a11y
Accessibility
The native element already exposes value semantics (indeterminate omits the current value); the component adds only a polite % readout and a generic-name fallback worth knowing about.
Keyboard
| Key | Action |
|---|---|
| — | Not a focus stop and no keys of its own — value changes announce through the polite status readout, never by stealing focus |
ARIA
| Attribute | Value | Description |
|---|---|---|
| progressbar (implicit) | native element | role, value mapping and the indeterminate state are the platform’s — measured: position 0.42 determinate, position −1 with no valuenow when omitted |
| aria-valuenow / min / max | native | Exposed by the element (measured aria-valuenow=100 at max=250); no ARIA attributes are authored |
| aria-label | label ?? "progress" | Names the bar; without a visible label the fallback is the GENERIC "progress" — pass a label when several bars share a page (anchoring note) |
| role: status | on the % readout | Polite live region — announced when the reader is idle; the value is announced through the relation, not read off the bar (measured "42%") |
| prefers-reduced-motion | split, measured | The stripe sweep slows to 4s (measured); the fill tween is NOT fully suppressible — the css transition is killed outright yet the visible tween persists (engine-side smoothing, css-unreachable; burst-measured across 3 runs) |