Stems UI
Menu

Progress

Displays the completion status of a task.

Export data
Waiting for server
Uploading
Codeapp/demos/public/progress.tsx
import { clientEntry, on, type Handle } from 'remix/component'
import { Progress, ProgressLabel, ProgressValue } from '@stemsui/registry/ui/progress.tsx'

export const ProgressDemo = clientEntry(import.meta.url, function ProgressDemo(handle: Handle) {
  let value = 13
  let running = true

  // Browser-only: advance the bar over time.
  handle.queueTask(() => {
    let timer = setInterval(() => {
      if (!running) return
      value = value >= 100 ? 0 : Math.min(100, value + Math.round(Math.random() * 15 + 5))
      void handle.update()
    }, 800)
    handle.signal.addEventListener('abort', () => clearInterval(timer))
  })

  return () => (
    <div class="flex w-full max-w-sm flex-col gap-8">
      <Progress value={value}>
        <ProgressLabel>Export data</ProgressLabel>
        <ProgressValue />
      </Progress>

      <Progress value={null}>
        <ProgressLabel>Waiting for server</ProgressLabel>
      </Progress>

      <Progress value={3} max={10} getAriaValueText={(_, current) => `${current} of 10 files`}>
        <ProgressLabel>Uploading</ProgressLabel>
        <ProgressValue>3 of 10 files</ProgressValue>
      </Progress>

      <button
        type="button"
        class="self-start text-sm text-muted-foreground underline underline-offset-4"
        mix={on('click', () => {
          running = !running
          void handle.update()
        })}
      >
        {running ? 'Pause' : 'Resume'}
      </button>
    </div>
  )
})

Installation

npx shadcn add @stemsui/progress

Requires the one-time setup. Behavior: stems-ui/progress.

API reference

The styled component is built on the headless module below: its props mirror these options, and the same events and data attributes apply.

stems-ui/progress

progress displays the status of a task that takes a long time, as an accessible progress bar.

Base UI equivalent: Progress.

Usage

import * as progress from 'stems-ui/progress'

function Export(handle: Handle<{ value: number | null }>) {
  return () => (
    <progress.Context value={handle.props.value}>
      <div class="grid grid-cols-2" mix={progress.root()}>
        <span mix={progress.label()}>Export data</span>
        <span class="text-right" mix={progress.value()}>
          <progress.ValueText />
        </span>
        <div class="col-span-2 h-1 bg-muted" mix={progress.track()}>
          <div class="h-full bg-primary transition-[width]" mix={progress.indicator()} />
        </div>
      </div>
    </progress.Context>
  )
}

stems-ui/progress

  • <progress.Context> props:
    • value: number | null. null means indeterminate.
    • min: defaults to 0. max: defaults to 100.
    • format: Intl.NumberFormatOptions for the value text.
    • locale: Intl.LocalesArgument. Defaults to the runtime locale.
    • getAriaValueText(formattedValue, value): returns aria-valuetext.
  • progress.root(): role="progressbar", aria-valuenow (omitted when indeterminate), aria-valuemin, aria-valuemax, aria-valuetext, and aria-labelledby pointing at label() (skipped when the host has aria-label).
  • progress.track(): the track. Status attributes only.
  • progress.indicator(): sets an inline style width: <percentage>% (plus Base UI's inset-inline-start: 0; height: inherit), merged with the host style. No width when indeterminate.
  • progress.value(): the host for the value text. Sets aria-hidden="true" (the root already exposes the value).
  • progress.label(): sets the id the root's aria-labelledby points at.
  • <progress.ValueText>: renders the formatted value as a text node, with no element of its own. Put it inside the value() host. Pass a function child to customize it: {(formattedValue, value) => …} (formattedValue is null when indeterminate).
  • formatValue(value, { format, locale }): the formatter, for use outside a Context.

Behavior notes

  • Mixins can't render children, so the value text comes from <progress.ValueText> (a hostless component reading the Context) or formatValue().
  • Formatting follows Base UI: without format, the value is formatted as a percent of 100 (value / 100 with style: 'percent'), whatever min/max are. Pass format for other units.
  • Default aria-valuetext: the formatted value, or "indeterminate progress".
  • The status is complete when value === max, progressing for other finite values, and indeterminate for null.
  • Server rendering: Base UI uses en-US until hydration. Stems uses the runtime locale on both sides, so pass locale if the server and browser locales might differ.
  • The root's aria-labelledby is always set (unless the host has aria-label). If there is no label() element, the reference doesn't resolve and assistive technology ignores it.

Styling hooks

All parts (root, track, indicator, value, label):

AttributeValues
data-completePresent when the progress has completed.
data-progressingPresent while the progress is ongoing.
data-indeterminatePresent when value is null.