Progress
Enhances a native progress element. Quiet is the native bar as a pencil hairline; warm is a small kolam drawn exactly as far as the value, closing at 100%; playful is a cutting-chai glass that fills with tea. It moves only when the value moves, and without a value it says in words what is happening.
quiet warm playful
Open the live demo
npx susegad add progress
Stands on: Core, Core: components, Engine, Kolam, Tokens
The prompt
the prompt
A kolam closes when the work is done; a glass of cutting chai is full when it is ready.
A determinate progress indicator that enhances the native <progress> element. The number is always in words beside the label. In warm, a small kolam is drawn exactly as far as the value and closes at 100%. In playful, a cutting-chai glass fills with tea.
<link rel="stylesheet" href="susegad/components/progress/progress.css">
<script type="module" src="susegad/components/progress/progress.js"></script>
<sg-progress label="Uploading photos">
<progress value="0.4" max="1">40%</progress>
</sg-progress>
<script>
upload.addEventListener('progress', e => { bar.value = e.loaded / e.total; });
</script>
The prompt
Build a progress indicator as a light-DOM custom element, <sg-progress label="…">, that enhances a native <progress> child, so the value and the progressbar role stay native and it still works without JavaScript. Put the label and the value in words above or beside the bar, and name the native element by the visible label with aria-labelledby. Give it three registers. Quiet: the native bar itself, styled as a three-pixel pencil hairline in the ink colour over a rule-coloured track, and nothing else. Warm: beside the words, a small SVG kolam, one unbroken line looping around thirteen dots, taken from the Kolam scene's mirror-curve geometry; draw the line exactly as far as the value with a dash offset over a faint dotted pencil guide of what is left, roughen it with a light turbulence filter so it looks hand inked, and turn the dots laterite when it closes at 100%. Playful: a fluted cutting-chai glass in SVG whose tea level is the value, clipped inside the glass, with its milky surface on top and three wisps of steam rising while motion is allowed. Move only with the work: animate only when the value changes, a short ease to the new value and no further, never on a timer. Without a value the element is indeterminate: say what is happening in words, draw no line and fill no tea; let the kolam's dots breathe slowly, or pour a thin stream into the empty glass. With reduced motion, draw the current value with no animation. Pause every animation while the element is off screen.
Words to code
| When you say | Technique | What happens |
|---|---|---|
enhances a native <progress> child | progressive enhancement | The browser's own element carries the value and the role. Without JavaScript, and in quiet, it is simply styled; the skins hide it visually but leave it for screen readers. |
| name the native element by the visible label | accessibility | The element writes the label into a visible span and points aria-labelledby at it, so the words people see are the words a screen reader says. The number beside it is aria-hidden because the native element already announces its value. |
| draw the line exactly as far as the value with a dash offset | stroke dashing | The kolam path has pathLength="100", so a dash offset of 60 leaves 40% drawn, whatever the path's real length. |
| taken from the Kolam scene's mirror-curve geometry | reuse | The warm skin calls the scene's pure geometry() for a thirteen-dot diamond instead of carrying its own drawing. |
| roughen it with a light turbulence filter | SVG filter | feTurbulence and feDisplacementMap nudge the line by about a pixel, painted once, so it reads as ink without redrawing. |
| tea level is the value, clipped inside the glass | clip path | One tall block of tea slides up and down behind a clip path shaped like the inside of the glass. An empty glass holds no tea at all. |
| animate only when the value changes | motion that follows the work | Each update eases from the value last shown to the new one with the Web Animations API, then stops. Nothing moves between updates. |
| say what is happening in words | indeterminate state | Without a value attribute the words say "Working on it" (or the quiet and playful wording), and no drawing suggests an amount. |
| Pause every animation while the element is off screen | budget | One shared IntersectionObserver tells each skin when it is visible; the steam and breathing animations are paused, not left running. |
<sg-progress> shows how far a piece of work has got: an upload, an import, a long save. It enhances a native <progress>, so the value and its meaning stay native. The number is always in words, and the drawing moves only when the value does.
Usage
<link rel="stylesheet" href="susegad/components/progress/progress.css">
<script type="module" src="susegad/components/progress/progress.js"></script>
<sg-progress label="Uploading photos">
<progress value="0.4" max="1">40%</progress>
</sg-progress>
Set the value from the work itself, never from a timer:
const bar = document.querySelector('sg-progress');
xhr.upload.addEventListener('progress', e => { bar.value = e.loaded / e.total; });
xhr.upload.addEventListener('load', () => { bar.value = 1; });
Setting the native element (progress.value = 0.4, or the value attribute from a server) works the same way. Remove the value (bar.value = null) when the amount is not known.
Attributes, properties and events
| Name | Kind | What it does |
|---|---|---|
label | attribute | The words shown above the bar, which also name it for screen readers. Without a label, give the native <progress> its own aria-label. |
register | attribute | quiet, warm or playful. Overrides the page's register for this element. |
value | property | The value on the native element, or null when indeterminate. Setting null removes the value. |
value, max | attributes on <progress> | As on the native element. max defaults to 1; value="60" max="100" is 60%. |
sg-complete | event | Fires once when the value reaches the maximum. Bubbles. detail: { label }. |
sg-skin | event | Fires when a register's drawing has loaded. |
Registers
- Quiet: the native bar, drawn as a pencil hairline in the ink colour on a rule-coloured track, with the label and "40%" above it. Without a value, a still dotted line and the words "In progress". This is also what shows without JavaScript.
- Warm: a small kolam beside the words, drawn exactly as far as the value over a faint dotted guide of what is left. When the value changes, the line eases on to the new value in about 0.4 s and stops. At 100% the loop closes and the dots turn the accent colour (laterite in the Susegad palette); the words say "Done". Without a value, no line is drawn and the dots breathe slowly beside "Working on it".
- Playful: a cutting-chai glass that fills with tea to the value, with a small slosh when it rises and steam curling off the top. Without a value, a thin stream pours into the empty glass beside "On its way". The label is set in the hand face.
Accessibility
- The native
<progress>is the progressbar. It is named by the visible label througharia-labelledby, and screen readers read its value themselves. The number beside the label isaria-hiddenso it is not read twice. - In warm and playful the native bar is visually hidden but stays in the accessibility tree. The drawings are
aria-hidden. - Without a value, the native element is indeterminate (screen readers say busy) and the visible words say what is happening.
- Nothing moves unless the value changes, except the indeterminate breathing dots and the steam. With reduced motion, every register shows the current value with no animation, and all animation pauses off screen.
- Zero axe violations in quiet, warm and playful, light and dark, desktop and phone.
What moves, and why
A determinate indicator moves only when its value changes. Do not feed it a timer or an estimate. If you do not know how far along the work is, leave the value off and use a clear label, or use <sg-loader>.
Credit
The warm kolam is the Kolam scene's own mirror-curve geometry; see that scene for the practice it comes from and who draws it. The cutting-chai glass is harvested from the Susegad sketchbook's chai plate.