Progress
A progress bar or capacity meter: a linear bar or circular ring with value thresholds that recolor and pulse.
Live demo
Progress shows how far along a task is, or how full a capacity is. The variant axis picks the shape (a horizontal linear bar or a circular svg ring) and the tone axis picks the color from the full roster (the six semantic roles, the accent variants, the twelve named hues).
A determinate bar fills to value between min and max; an indeterminate mode animates a moving sweep when the amount of work is unknown. Declarative <threshold below tone pulse> children turn it into a self-coloring meter: each band names a percentage ceiling, and the active band (the first the current value falls under) overrides the tone and can pulse the fill (slow or fast) to flag a critical level, the pulse routed through motion tokens so the reduced-motion base rule stills it. Set meter to report role="meter" (a measurement against a capacity, like disk used) instead of the default role="progressbar" (a task advancing); either way it carries aria-valuenow/aria-valuemin/aria-valuemax. An optional inline readout (show-value) shows the percentage, the raw value, or value/max (value-format), sits at the end or inset over the bar (value-position), and can take the active tone (colorize-value).
When to use
How this component composes with the rest of the set.
Props
24 props, straight from the manifest.
| Prop | Type | Default | Bindings | Description |
|---|---|---|---|---|
sm thins the bar or shrinks the ring, lg fattens it. | ||||
min and max. | ||||
aria-valuenow. | ||||
show-value reads: percent (80%), value (the raw number), or value-max (80/100). | ||||
value / value-max readout (e.g. GB); the percent format ignores it (the % is the unit). | ||||
show-value readout sits: after the bar (end) or laid over the fill (inset). | ||||
show-value readout with the active tone (the threshold tone when a band is active). | ||||
role="meter" (a measurement against a capacity, e.g. disk used) instead of role="progressbar" (a task advancing); the visual treatment is unchanged. | ||||
tone: a built-in palette (intensity, thermal, severity), a JSON array of stop colors (["#00f","#f00"]), or a comma-separated stop list. Reinforces magnitude with temperature, so a fuller bar also reads hotter. | ||||
ramp paints: solid samples one color at the current value off the live cascade (needs the runtime); gradient paints the whole scale as a pure-CSS sweep clipped to the fill (zero-JS, SSR-safe, linear only, a circular ring falls back to solid). | ||||
ramp end for end (hot-to-cold). | ||||
<threshold below="<pct>" tone="<tone>" pulse="slow|fast"> names a percentage ceiling; the active band (the first the current value falls under, bands sorted ascending) overrides tone and may pulse the fill; place them as direct children of the element. | ||||
true keeps the default --neutral-bg rail, false (track="none" in HTML) drops it entirely, and a tone paints it that tone's -bg. A ring reporting a window that may not exist reads better trackless, where an always-on groove reads as a border rather than an empty meter. | ||||
circular ring reads, independent of its diameter. A unitless number is in ring units (the ring is a 40-unit viewBox at r=16), so the weight scales with the ring; a CSS length (6px, 0.25rem) is absolute and holds the same apparent weight at any size. Sets --xtyle-progress-stroke, which CSS can set directly instead. Circular only; a linear rail's height still follows size. | ||||
ariaLabel, which names the meter for a screen reader and renders nothing: two bars stacked in a panel are told apart by AT and not by eye unless one of them says what it is. Setting label alone also names the meter, so the string is written once; an explicit ariaLabel still wins. | ||||
valueFormat, because a meter's most useful reading is often not a number the component could have computed (8/20, even, yours, 46 left). The built-in showValue readout is a different thing in a different place: it sits on the bar and speaks the number. | ||||
vertical grows the indicator up the block axis natively, so a standing gauge needs no rotation: a rotated element keeps its pre-rotation layout box, so its visible height is its pre-rotation width and no percentage resolves against the side you can see. The rail takes its length from --xtyle-progress-length, the way a ring takes its diameter from --xtyle-progress-size. Linear only; a ring has no axis to stand up. | ||||
label is absent or the announced name should differ from the visible one. One of the two is required: a progressbar with no name is not announced. |
Appearance
Variants
linear
A horizontal bar that fills left-to-right.
circular
An SVG ring whose stroked arc sweeps clockwise from the top. Its diameter follows size unless --xtyle-progress-size overrides it, and its stroke weight follows thickness / --xtyle-progress-stroke, so a ring can be large and thin or small and heavy.
Sizes
sm
Thin bar / small ring.
md
Default.
States
indeterminate
Work of unknown duration; the indicator animates a continuous sweep.
pulse
An active <threshold pulse> band; the fill breathes (slow or fast) to flag a critical level, stilled under reduced-motion.
focus-visible
Keyboard focus: a token-colored ring, plus a transparent outline that becomes real in forced-colors mode.
Anatomy
The named parts that make up the component. A ::part() handle is reachable from your own stylesheet; the class beside it is the component's internal selector, which a shadow boundary keeps to itself.
progress
The root element carrying the variant, tone, and size classes. It stacks the caption, the bar, and the note; the role lives on the bar rather than here, so a caption is prose beside the measurement rather than content inside it.
bar
The measuring element, carrying the progressbar (or meter) role and the ARIA value state, and holding the track, the indicator, and the readout.
caption
The line above the bar: the visible label on one side, the free-form reading on the other. Rendered only when one of them is set or slotted.
label
The visible name of the measurement, on the caption line.
reading
The free-form reading opposite the label, tabular so a column of them lines up.
note
A line of prose under the bar, explaining what the reading means. Rendered only when set or slotted.
track
The unfilled groove the indicator runs along: a bar rail (.xtyle-progress__track) or an SVG ring (.xtyle-progress__track-ring). Both variants expose it as ::part(track), and track can tone it or drop it entirely.
indicator
The filled portion, colored by tone: a bar segment or a stroked arc.
value
The optional inline readout shown when show-value is set.
Tokens & coverage
What the component consumes, checked live against what the algorithm produces.
Live coverage check against the xtyle-default register
(derive(xtyleDefault, { anchors }) →
coverComponent(manifest, register)). Every token this component
consumes must be a key the algorithm produces.
--accent--accent-2--accent-2-bg--accent-3--accent-3-bg--accent-4--accent-4-bg--accent-bg--bg-0--black--black-bg--blue--blue-bg--border-normal--border-thick--brown--brown-bg--cyan--cyan-bg--danger--danger-bg--duration-base--ease-emphasized--ease-standard--fg-0--fg-1--fg-2--font-sans--gray--gray-bg--green--green-bg--info--info-bg--leading-normal--leading-tight--neutral--neutral-bg--neutral-bg--orange--orange-bg--pink--pink-bg--purple--purple-bg--radius-full--radius-sm--red--red-bg--ring--space-1--space-2--space-3--space-6--space-8--success--success-bg--text-body--text-sm--text-xs--warn--warn-bg--white--white-bg--yellow--yellow-bgSlots
Optional custom content for the readout, in place of the built-in show-value text (e.g. a formatted label). The Svelte binding fills it with the readout snippet, since value there is already the numeric prop.
Markup for the caption's name, in place of the plain label string. The Svelte binding fills it with the labelContent snippet.
Markup for the caption's reading, in place of the plain reading string. The Svelte binding fills it with the readingContent snippet.
Markup for the note under the bar, in place of the plain note string. The Svelte binding fills it with the noteContent snippet.
The <threshold below tone pulse> config children (hidden from view); the bar reads them to recolor and pulse by value.
Accessibility
Code
Linear and circular
Both shapes carry any tone; add show-value for a readout or indeterminate for unknown work.
<xtyle-progress value="42" aria-label="Upload progress"></xtyle-progress>
<xtyle-progress value="80" tone="success" show-value aria-label="Storage used"></xtyle-progress>
<xtyle-progress variant="circular" value="65" tone="info" show-value aria-label="Sync"></xtyle-progress>
<xtyle-progress variant="circular" indeterminate aria-label="Loading"></xtyle-progress><script lang="ts">
import { Progress } from "@xtyle/svelte";
let value = $state(42);
</script>
<Progress {value} ariaLabel="Upload progress" />
<Progress value={80} tone="success" showValue ariaLabel="Storage used" />
<Progress variant="circular" value={65} tone="info" showValue ariaLabel="Sync" />
<Progress variant="circular" indeterminate ariaLabel="Loading" />---
import { Progress } from "@xtyle/astro";
---
<Progress value={42} aria-label="Upload progress" />
<Progress value={80} tone="success" showValue aria-label="Storage used" />
<Progress variant="circular" value={65} tone="info" showValue aria-label="Sync" />
<Progress variant="circular" indeterminate aria-label="Loading" />A capacity meter with thresholds
Set meter for the measurement role and add <threshold below tone pulse> children: the bar greens under 75%, ambers past it, and reds and pulses once it crosses 90% full, the value/max readout carrying its unit. This is the gauge use once served by a separate Meter.
<xtyle-progress meter value="910" max="1000" show-value value-format="value-max" unit=" GB" colorize-value aria-label="Disk usage">
<threshold below="75" tone="success"></threshold>
<threshold below="90" tone="warn"></threshold>
<threshold below="101" tone="danger" pulse="fast"></threshold>
</xtyle-progress><Progress meter value={910} max={1000} showValue valueFormat="value-max" unit=" GB" colorizeValue ariaLabel="Disk usage">
<threshold below="75" tone="success" />
<threshold below="90" tone="warn" />
<threshold below="101" tone="danger" pulse="fast" />
</Progress><Progress meter value={910} max={1000} showValue valueFormat="value-max" unit=" GB" colorizeValue aria-label="Disk usage">
<threshold below="75" tone="success" />
<threshold below="90" tone="warn" />
<threshold below="101" tone="danger" pulse="fast" />
</Progress>Saying what it is, and standing it up
aria-label names a meter for a screen reader and renders nothing, so two bars stacked in a panel are told apart by AT and not by eye. label renders the name and announces it, reading is free-form so it can say what no value format could compute (even, 46 left), and note explains the reading. orient="vertical" grows the fill up the block axis natively; give the host a height and the rail fills it.
<xtyle-progress
label="The haggle"
reading="8 of 20 coins"
note="They will come down another two, and no further."
value="40"
></xtyle-progress>
<xtyle-progress label="Standing" reading="even" value="50" tone="info"></xtyle-progress>
<div style="--xtyle-progress-length: 9rem">
<xtyle-progress orient="vertical" label="Fuel" reading="34%" value="34" tone="info"></xtyle-progress>
</div><script lang="ts">
import { Progress } from "@xtyle/svelte";
</script>
<Progress
label="The haggle"
reading="8 of 20 coins"
note="They will come down another two, and no further."
value={40}
/>
<Progress label="Standing" reading="even" value={50} tone="info" />
<div style="--xtyle-progress-length: 9rem">
<Progress orient="vertical" label="Fuel" reading="34%" value={34} tone="info" />
</div>
<Progress label="Shipment" value={62}>
{#snippet readingContent()}<strong>62</strong> of 100 crates{/snippet}
</Progress>---
import { Progress } from "@xtyle/astro";
---
<Progress
label="The haggle"
reading="8 of 20 coins"
note="They will come down another two, and no further."
value={40}
/>
<Progress label="Standing" reading="even" value={50} tone="info" />
<div style="--xtyle-progress-length: 9rem">
<Progress orient="vertical" label="Fuel" reading="34%" value={34} tone="info" />
</div>
<Progress label="Shipment" value={62}>
<Fragment slot="reading"><strong>62</strong> of 100 crates</Fragment>
</Progress>A fill that colors by its value
Set ramp to color the fill along a scale by its own value instead of a flat tone: solid (the default) samples one color at the current value, so a busier bar reads hotter; ramp-mode="gradient" sweeps the whole scale as pure CSS. Both track the theme's own hues, so the ramp restyles with the algorithm.
<xtyle-progress ramp="thermal" value="30" aria-label="Load, cool"></xtyle-progress>
<xtyle-progress ramp="thermal" value="70" aria-label="Load, warm"></xtyle-progress>
<xtyle-progress ramp="thermal" value="95" aria-label="Load, hot"></xtyle-progress>
<xtyle-progress ramp="thermal" ramp-mode="gradient" value="80" aria-label="Capacity sweep"></xtyle-progress><Progress ramp="thermal" value={30} ariaLabel="Load, cool" />
<Progress ramp="thermal" value={70} ariaLabel="Load, warm" />
<Progress ramp="thermal" value={95} ariaLabel="Load, hot" />
<Progress ramp="thermal" rampMode="gradient" value={80} ariaLabel="Capacity sweep" /><Progress ramp="thermal" value={30} aria-label="Load, cool" />
<Progress ramp="thermal" value={70} aria-label="Load, warm" />
<Progress ramp="thermal" value={95} aria-label="Load, hot" />
<Progress ramp="thermal" rampMode="gradient" value={80} aria-label="Capacity sweep" />A ring as a frame
A circular meter sized to its container reads as a frame rather than a widget. track="none" drops the groove (an always-on rail around a portrait reads as a border, not an empty meter), --xtyle-progress-size takes the diameter off the size scale, and thickness sets the band weight independent of that diameter, so a large ring can stay thin. Both variants expose ::part(track), so a mod or an app can reach the groove either way.
<style>
.portrait { position: relative; width: 96px; height: 96px; }
.portrait xtyle-progress { display: block; position: absolute; inset: 0; --xtyle-progress-size: 100%; }
</style>
<div class="portrait">
<xtyle-avatar src="/portrait.jpg" alt="Ada" size="lg"></xtyle-avatar>
<xtyle-progress variant="circular" value="72" track="none" thickness="4px" tone="success" aria-label="Profile complete"></xtyle-progress>
</div>
<xtyle-progress variant="circular" value="40" track="info" thickness="1.5" aria-label="A toned groove, thin band"></xtyle-progress><script lang="ts">
import { Avatar, Progress } from "@xtyle/svelte";
</script>
<div class="portrait">
<Avatar src="/portrait.jpg" alt="Ada" size="lg" />
<Progress variant="circular" value={72} track={false} thickness="4px" tone="success" ariaLabel="Profile complete" />
</div>
<Progress variant="circular" value={40} track="info" thickness={1.5} ariaLabel="A toned groove, thin band" />
<Progress variant="circular" value={72} showValue ariaLabel="Sync">
{#snippet readout()}<strong>72 of 100</strong>{/snippet}
</Progress>
<style>
.portrait { position: relative; width: 96px; height: 96px; }
.portrait :global(xtyle-progress) { display: block; position: absolute; inset: 0; --xtyle-progress-size: 100%; }
</style>---
import { Avatar, Progress } from "@xtyle/astro";
---
<div class="portrait">
<Avatar src="/portrait.jpg" alt="Ada" size="lg" />
<Progress variant="circular" value={72} track={false} thickness="4px" tone="success" aria-label="Profile complete" />
</div>
<Progress variant="circular" value={40} track="info" thickness={1.5} aria-label="A toned groove, thin band" />
<Progress variant="circular" value={72} showValue aria-label="Sync">
<strong slot="value">72 of 100</strong>
</Progress>
<style>
.portrait { position: relative; width: 96px; height: 96px; }
.portrait xtyle-progress { display: block; position: absolute; inset: 0; --xtyle-progress-size: 100%; }
</style>