Canvas Design System
Main Site Tokens

Progress

Shows how far along something is, as a proportion of a known whole.

When to Use

Use when: You can express the state as a percentage — an upload, a multi-step form, a completion score. Use the indeterminate mode when work is genuinely underway but unmeasurable.
Don't use when: You're waiting for content whose shape you know — Skeleton gives a better sense of what's coming. Don't use it as a decorative meter for a rating or a score with no notion of "complete".

Variants

Colour

Five variants. Colour carries meaning, so drive it from the state rather than the page's palette — success at 100%, error when something failed part-way.

Primary — 75%
Success — 100%
Warning — 50%
Error — 25%
Info — 60%
<?= ws_progress(75, ['variant' => 'primary']) ?>
<?= ws_progress(100, ['variant' => 'success']) ?>

Sizes

Small
Medium (default)
Large
<?= ws_progress(60, ['size' => 'sm']) ?>

With label and value

The label doubles as the bar's accessible name, so set it whenever the surrounding text doesn't already explain what's progressing.

Uploading file… 65%
Complete 100%
<?= ws_progress(65, [
    'variant'   => 'primary',
    'label'     => 'Uploading file…',
    'showValue' => true,
]) ?>

States

Striped and animated

animated requires striped — the animation moves the stripes, so on its own it does nothing visible.

Striped
Animated stripes
<?= ws_progress(70, ['striped' => true, 'animated' => true]) ?>

Indeterminate

For work with no measurable proportion. The bar loops continuously, the width style is dropped, and the percentage is suppressed even if showValue is set.

Loading…
<?= ws_progress(0, ['indeterminate' => true, 'label' => 'Loading…']) ?>
StateBehavior
Value out of rangeClamped with max(0, min(100, $value)), so a stray 150 or −10 renders as 100 or 0 rather than overflowing the track.
ZeroRenders an empty track. Visually identical to "not started", which is usually what you want.
CompleteNo automatic treatment at 100% — switch to variant: 'success' yourself if completion should read differently.
IndeterminateAdds .ws-progress__bar--indeterminate and omits the inline width. aria-valuenow is also omitted, so no false percentage is announced.
Reduced motionHonored. Under prefers-reduced-motion: reduce both loops stop; the indeterminate bar fills its track at reduced opacity so it still reads as in-progress.

Real-World Usage

A workshop's setup completion on MyWorkshopr: value derived from real task state, label naming what's measured, and the variant flipping to success once everything's done.

Workshop setup 60%
<?php
$done  = count(array_filter($tasks, fn($t) => $t['done']));
$pct   = $tasks ? round($done / count($tasks) * 100) : 0;
?>
<?= ws_progress($pct, [
    'variant'   => $pct === 100 ? 'success' : 'primary',
    'label'     => 'Workshop setup',
    'showValue' => true,
]) ?>

Options

OptionTypeDefaultPurpose
$valueint|float0Positional. 0–100, clamped to that range.
variantstring'primary'primary | success | warning | error | info
sizestring'md'sm | md | lg — track height
labelstringnullVisible label, also used as the bar's aria-label
showValueboolfalseShow the percentage. Suppressed when indeterminate.
stripedboolfalseDiagonal stripe pattern
animatedboolfalseAnimates the stripes. Has no effect without striped.
indeterminateboolfalseContinuous loop for unmeasurable work
idstringnullElement ID
classstring''Additional CSS classes
attrsarray[]Extra HTML attributes as key/value pairs

Accessibility

ConcernBehavior
ARIAEmits role="progressbar" with aria-valuemin="0" and aria-valuemax="100". aria-valuenow is added for determinate bars and correctly omitted when indeterminate is set.
LabellingWhen label is set it becomes aria-label. Without one the bar is announced as an unnamed progressbar, so pass a label unless neighbouring text clearly names it.
KeyboardNot focusable, which is correct — a progress bar reports state and accepts no input.
Live updatesUpdating aria-valuenow in the DOM is announced by most screen readers without a live region. Don't wrap the bar in aria-live="assertive" — that produces a stream of interruptions on every tick.
Colour independenceVariant colour is the only signal distinguishing error from success at the same percentage. Put the state in the label too.
check_circle Fixed July 2026. Two gaps closed: aria-valuenow was emitted even when indeterminate, so assistive tech announced a bogus percentage for work of unknown length; and neither animation honoured prefers-reduced-motion, unlike Skeleton. Both now behave correctly.

Tokens

TokenUsed for
--gray-100Track background
--mainsite-gradientPrimary bar fill
--color-success / --color-success-darkSuccess bar
--color-warning / --color-warning-darkWarning bar
--color-error / --color-error-darkError bar
--color-info / --color-info-darkInfo bar
--radius-fullRounded track and bar ends
--text-dark / --text-defaultLabel and value text
--text-metaLabel and value type scale
--font-body / --font-medium / --font-semiboldFace and weights
--transition-baseWidth transition as the value changes

CSS Classes

ClassPurpose
.ws-progress-wrapperOuter wrapper holding the label row and the track
.ws-progressThe track — carries role="progressbar"
.ws-progress--sm / --md / --lgTrack height
.ws-progress__labelLabel text
.ws-progress__valuePercentage readout
.ws-progress__barThe filled portion
.ws-progress__bar--primary / --success / --warning / --error / --infoBar colours
.ws-progress__bar--stripedStripe pattern
.ws-progress__bar--animatedMoving stripes (needs --striped)
.ws-progress__bar--indeterminateContinuous loop

Files

FilePurpose
includes/components/helpers.phpws_progress() helper function
includes/components/progress.phpTemplate — clamping, ARIA, and class assembly
includes/components/components.cssStyles and the stripe/indeterminate keyframes (.ws-progress rules)