KKUONIDesign System

Components

ProgressBar

Beta

A determinate or indeterminate progress indicator used for multi-step booking flows, search loading states, and upload or processing feedback.

Booking progress60%

Purpose

ProgressBar communicates where a user or system stands in a process that takes measurable time or discrete steps. In a travel booking context this appears in two primary scenarios:

Determinate — the system knows the total duration or step count: "Step 2 of 4 — Passenger details", "Uploading passport scan (78%)", "Payment authorisation (45 s remaining)". The fill moves incrementally as the process advances.

Indeterminate — the system is working but cannot estimate completion: "Searching 2,400 holidays", "Fetching live pricing", "Confirming with supplier". An animated sliding bar communicates activity without implying a known duration.

ProgressBar is a passive indicator — it informs the user, it does not accept input. For stepped form navigation with explicit back/next controls, pair it with a StepIndicator component rather than replacing it.

Usage

Use ProgressBar whenever a process has a measurable duration or step count that the user is waiting on — booking steps, uploads, live search. For a compact, non-labelled loading indicator with no meaningful progress value (e.g. a small inline spinner while a button submits), use Spinner instead.

ProgressBar typically appears at the top of a multi-step checkout form, inside a loading state for search results, or alongside an upload or processing indicator.

Anatomy

Step 1 of 4 — Choose your holiday25%
Searching available flights
  1. 1
    LabelA visible text label above the bar describing what is progressing. Always provide a label — the track alone does not communicate context.
  2. 2
    Value textOptional percentage shown to the right of the label row when showValue is true. Rounds to the nearest whole number.
  3. 3
    TrackThe full-width pill-shaped container in bg-grey-100. Height varies by size prop: 4px (sm), 8px (md, default), 12px (lg).
  4. 4
    FillThe coloured portion of the track representing current progress. Colour is determined by the variant prop. For indeterminate, this fills approximately 30% of the track width and slides end-to-end on a 1.5s loop.

Variants

Semantic fill colours

Booking confirmed100%
Payment processing60%
Awaiting supplier confirmation40%
Payment declined — retrying30%
VariantFill colourUse for
defaultbg-tealStandard progress — booking steps, uploads
successbg-successCompleted step or full confirmation
warningbg-warningPartial completion, pending external action
dangerbg-dangerFailure state, retry in progress

Sizes

Small — compact status bar
Medium — default (recommended)
Large — hero or onboarding flows
SizeTrack heightUse for
sm4 pxCompact layouts, card footers, inline status
md8 pxDefault — checkout flows, step indicators
lg12 pxHero onboarding, standalone progress screens

Indeterminate

Use indeterminate when the system is working but cannot report a percentage. The fill element animates from left to right in a continuous 1.5 s loop using animate-[slide_1.5s_ease-in-out_infinite].

Searching 2,400 holidays for you
Fetching live flight prices

When indeterminate is true, the value prop is ignored for display purposes but is still passed to aria-valuenow as undefined, which is correct per the ARIA spec for indeterminate progress.


Booking step flow

A common pattern on Kuoni checkout — a determinate bar with a descriptive label that updates as the user moves through steps.

Step 2 of 4 — Passenger details50%
Step 3 of 4 — Extras and upgrades75%
Step 4 of 4 — Payment100%

States

StateDescription
DeterminateFill width = (value / max) * 100%. Value clamped between 0 and max.
IndeterminateFill animates at fixed width (~30% of track). value prop has no visual effect.
Completevalue === max. Switch variant to 'success' to confirm completion visually.
ErrorSwitch variant to 'danger' to signal a failed operation. Update label to describe the error.

Best practices

Do

Always provide a descriptive label that explains what is progressing — not just 'Loading' or 'Please wait'.

A label like 'Step 2 of 4 — Passenger details' tells the traveller exactly where they are and what comes next.

Don't

Render a ProgressBar without a label prop or a visible heading nearby that explains the process.

A bare progress bar with no label forces the user to infer context from surrounding elements they may not see.

Do

Switch from indeterminate to determinate as soon as the system can report a real percentage.

Use indeterminate only when the system genuinely cannot compute a percentage — it signals 'working' not 'almost done'.

Don't

Display a determinate bar when the value is estimated, fabricated, or does not reflect real progress.

A progress bar stuck at 0% or jumping non-linearly from 10% to 90% destroys user trust.

Do

Use variant='success' when the process completes to give a positive completion signal.

Switching to success variant at 100% provides a distinct visual confirmation without requiring a separate UI element.

Don't

Leave the variant as 'default' when the process completes if a success state is available.

A teal bar at 100% looks identical to one at 50% — the user has to read the label to know it is done.

Do

Show the value percentage for longer-running processes (over 3 s) where the exact number is reassuring.

showValue adds a precise percentage for processes where the traveller is waiting and wants to know how close to done they are.

Don't

Show the percentage value when the underlying data changes infrequently or unpredictably.

A percentage that jumps or increments in large blocks is more alarming than reassuring.

Props

PropTypeDefaultDescription
valuenumber0Current progress value. Must be between 0 and max. Ignored visually when indeterminate is true, but still passed to aria-valuenow.
maxnumber100Maximum value of the progress range. The fill percentage is calculated as (value / max) * 100.
labelstringVisible label text shown above the track. Always provide this for context. Also used as the accessible label via aria-label when no external label element is present.
showValuebooleanfalseWhen true, renders the percentage as text to the right of the label row. Rounds to the nearest whole number. Has no effect when indeterminate is true.
size'sm' | 'md' | 'lg''md'Track height. sm=4px, md=8px, lg=12px. The fill element matches the track height.
variant'default' | 'success' | 'warning' | 'danger''default'Fill colour. Use success at 100% completion, warning for pending external action, and danger for error or failure states.
indeterminatebooleanfalseWhen true, the fill animates in a continuous slide loop. Use when progress cannot be measured — e.g. searching, fetching live prices. Overrides value for display.
classNamestringAdditional Tailwind classes merged onto the root wrapper element.

Accessibility

ARIA role and attributes

The track element renders as role="progressbar" with the following attributes:

<div
  role="progressbar"
  aria-valuenow={indeterminate ? undefined : value}
  aria-valuemin={0}
  aria-valuemax={max}
  aria-label={label}
/>

For indeterminate bars, aria-valuenow is omitted entirely (not set to 0). The ARIA spec states that omitting aria-valuenow on a progressbar signals an indeterminate state to assistive technology.

Live announcements

ProgressBar does not include an aria-live region by default — the progressbar role is sufficient for screen readers to announce state changes. If you need periodic spoken updates (e.g. "75% complete" announced at 25%, 50%, 75%), add an aria-live="polite" region outside the component and update it at the appropriate thresholds in your consuming code.

Example pattern for announced milestones:

const [liveText, setLiveText] = useState('');

// In your progress update handler:
if (value === 50) setLiveText('Search half complete');
if (value === 100) setLiveText('Search complete. 47 holidays found.');

return (
  <>
    <ProgressBar label="Searching holidays" value={value} showValue />
    <span className="sr-only" aria-live="polite">{liveText}</span>
  </>
);

Reduced motion

The indeterminate slide animation respects the prefers-reduced-motion media query. When the user has requested reduced motion, the animation is paused and the fill bar is displayed as a static half-width block. This still communicates 'working' without triggering vestibular discomfort.

Colour contrast

The track and fill colours meet WCAG 2.1 AA for UI components (3:1 ratio against the page background).

VariantFill colourTrack colourAgainst whiteWCAG
defaultTeal (#005B55)#E5E5E57.4:1AA
successGreen (#2E7D32)#E5E5E56.1:1AA
warningAmber (#B45309)#E5E5E54.5:1AA
dangerRed (#B91C1C)#E5E5E55.1:1AA
  • Spinner — a compact, indeterminate loading indicator without a measurable progress value; use for short waits where a full labelled bar is unnecessary.