Progress
On this page
Use a determinate bar whenever you know how much is left — it is the only one that tells the reader anything. Omit value and the bar animates instead, which says "still working" and nothing more.
Determinate
stack({ gap: 'lg' },
progress({ value: 25 }),
progress({ value: 60 }),
progress({ value: 100 }),
)Indeterminate
progress()A bar with no label is marked aria-hidden — a progressbar role with no accessible name tells a screen reader nothing, so an unlabelled bar is treated as decoration. Label anything a reader is meant to follow.
Labels
A label names what is happening; showValue adds the percentage on the right.
stack({ gap: 'lg' },
progress({ value: 72, label: 'Rendering pages', showValue: true }),
progress({ value: 30, max: 60, label: 'Optimising images', showValue: true }),
progress({ label: 'Waiting for the deploy' }),
)Colors and height
stack({ gap: 'lg' },
progress({ value: 80, color: 'success', label: 'Passed', showValue: true }),
progress({ value: 45, color: 'warning', label: 'Degraded', showValue: true }),
progress({ value: 20, color: 'danger', label: 'Failing', showValue: true }),
progress({ value: 60, color: 'neutral', height: 'xs' }),
progress({ value: 60, color: 'primary', height: '1rem' }),
)A scale other than 100
max lets you pass the raw numbers — pages built out of pages total — instead of working out a percentage first.
progress({ value: 118, max: 169, label: '118 of 169 pages', showValue: true })Moving it from the browser
A bar is server-rendered HTML: the percentage is a custom property on the fill and a number in aria-valuenow, and nothing on the page changes either on its own. Give the bar an id and setProgress moves both together — the fill, the announced value, and the percentage beside the label.
import { setProgress } from 'sitelo/ui/client'
const request = new XMLHttpRequest()
request.upload.addEventListener('progress', (event) => {
setProgress('upload', event.loaded, { max: event.total })
})The maximum is remembered, so later calls are just a value. Or reach the module the way the components reach theirs, and skip the bundle entirely:
button({ onclick: "import('/su/progress.js').then(m=>m.set('upload',100))" }, 'Finish')Passing null — or anything that is not a finite number — hands the bar back to the indeterminate animation, so work that stops reporting numbers does not have to be special-cased. getProgress() reads the current value back, on the bar’s own scale.
Try it
This page loads the runtime, so the buttons below really do move the bar.
stack({ gap: 'md' },
progress({ id: 'demo-progress', value: 0, label: 'Uploading', showValue: true }),
stack({ direction: 'row', gap: 'sm', wrap: true },
button({ size: 'sm', variant: 'outline', onclick: "import('/su/progress.js').then(m=>m.set('demo-progress',0))" }, 'Reset'),
button({ size: 'sm', variant: 'outline', onclick: "import('/su/progress.js').then(m=>m.set('demo-progress',35))" }, '35%'),
button({ size: 'sm', variant: 'outline', onclick: "import('/su/progress.js').then(m=>m.set('demo-progress',80))" }, '80%'),
button({ size: 'sm', variant: 'outline', onclick: "import('/su/progress.js').then(m=>m.set('demo-progress',100))" }, 'Done'),
button({ size: 'sm', variant: 'ghost', onclick: "import('/su/progress.js').then(m=>m.set('demo-progress',null))" }, 'Unknown'),
),
)An unlabelled bar is moved too, but it stays aria-hidden — it was rendered without a name on purpose, and announcing a value on it now would put a nameless progressbar in the accessibility tree.
Spinner
There is no spinner component — the spinner is an icon, and spin is what turns it. Like every icon it is sized in em, so it matches whatever text it sits beside without being told a size.
stack({ direction: 'row', gap: 'lg', align: 'center' },
icon('spinner', { spin: true, size: 'sm' }),
icon('spinner', { spin: true }),
icon('spinner', { spin: true, size: 'lg' }),
)Spinner in context
Give a standalone spinner a label so it is announced. One inside a button does not need one — the button already says what it is doing.
Fetching the latest build…
stack({ gap: 'md' },
stack({ direction: 'row', gap: 'sm', align: 'center' },
icon('spinner', { spin: true, label: 'Loading' }),
text({ variant: 'small', tone: 'muted' }, 'Fetching the latest build…'),
),
stack({ direction: 'row', gap: 'sm' },
button({ loading: true }, 'Deploying'),
button({ variant: 'outline', loading: true }, 'Checking links'),
),
)Props
progress() — exported as progressBar too:
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | — | How far along. Omit for the indeterminate animation. |
max | number | 100 | What value counts as complete. |
color | 'primary' | 'neutral' | 'success' | 'warning' | 'danger' | 'primary' | Fill colour. |
label | Child | — | Text above the bar; also its accessible name. |
showValue | boolean | false | Show the percentage beside the label. |
height | Space | '0.5rem' | Bar thickness. |
setProgress() from sitelo/ui/client:
| Prop | Type | Default | Description |
|---|---|---|---|
target | Element | string | — | The bar, or the id of one. A selector is tried if no element has that id. |
value | number | null | — | Where to move it. null returns it to the indeterminate animation. |
options.max | number | 100 | What counts as complete. Remembered for the calls after it. |
The spinner has no props of its own — it is icon('spinner', { spin: true }), and takes whatever icon() takes.