Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

FeedbackReact

Progress Group

Shows the stages of one task on a single bar, such as uploading, processing and publishing a video.

Rendered by React 19.3.1

import * as React from 'react'
import { ProgressGroup } from '@hamur/react/progress-group'

const stages = ['Upload', 'Process', 'Publish']
const steps = [
  [20, 0, 0],
  [40, 0, 0],
  [40, 20, 0],
  [40, 35, 0],
  [40, 35, 15],
  [40, 35, 25],
]

export default function ProgressGroupExample() {
  const [step, setStep] = React.useState(0)
  return (
    <div className="stack">
      <ProgressGroup.Root className="range" value={steps[step]}>
        <div className="range-head">
          <ProgressGroup.Label>Publishing the video</ProgressGroup.Label>
          <ProgressGroup.Value className="muted" />
        </div>
        <ProgressGroup.Track className="range-track range-group-track">
          {stages.map((stage, index) => (
            <ProgressGroup.Indicator
              key={stage}
              className="range-group-fill"
              index={index}
            />
          ))}
        </ProgressGroup.Track>
        <div className="range-group-legend">
          {stages.map((stage, index) => (
            <ProgressGroup.Item
              key={stage}
              className="range-group-item"
              index={index}
            >
              <ProgressGroup.ItemLabel>{stage}</ProgressGroup.ItemLabel>
              <ProgressGroup.ItemValue className="muted" />
            </ProgressGroup.Item>
          ))}
        </div>
      </ProgressGroup.Root>
      <div className="row">
        <button
          type="button"
          className="button"
          onClick={() => setStep((current) => Math.min(steps.length - 1, current + 1))}
        >
          Next step
        </button>
        <button type="button" className="button" onClick={() => setStep(0)}>
          Reset
        </button>
      </div>
    </div>
  )
}
Not measured yetTests passing in React
Not measured yetSize
Not measured yetMemory
Not measured yetSpeed
[N] commits

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<ProgressGroup.Root>
  <ProgressGroup.Label />
  <ProgressGroup.Value />
  <ProgressGroup.Track>
    <ProgressGroup.Indicator />
  </ProgressGroup.Track>
  <ProgressGroup.Item>
    <ProgressGroup.ItemLabel />
    <ProgressGroup.ItemValue />
  </ProgressGroup.Item>
</ProgressGroup.Root>

Examples

Indeterminate

A backup whose amounts are unknown until scanning finishes; the track animates instead.

ProgressGroupIndeterminate.tsx
import * as React from 'react'
import { ProgressGroup } from '@hamur/react/progress-group'

const folders = ['Photos', 'Documents']

export default function ProgressGroupIndeterminateExample() {
  const [value, setValue] = React.useState<number[] | null>(null)
  return (
    <div className="stack">
      <ProgressGroup.Root className="range" value={value}>
        <div className="range-head">
          <ProgressGroup.Label>Backing up</ProgressGroup.Label>
          <ProgressGroup.Value className="muted" />
        </div>
        <ProgressGroup.Track className="range-track range-group-track">
          {folders.map((folder, index) => (
            <ProgressGroup.Indicator key={folder} className="range-group-fill" index={index} />
          ))}
        </ProgressGroup.Track>
        <div className="range-group-legend">
          {folders.map((folder, index) => (
            <ProgressGroup.Item key={folder} className="range-group-item" index={index}>
              <ProgressGroup.ItemLabel>{folder}</ProgressGroup.ItemLabel>
              <ProgressGroup.ItemValue className="muted" />
            </ProgressGroup.Item>
          ))}
        </div>
      </ProgressGroup.Root>
      <div className="row">
        <button type="button" className="button" onClick={() => setValue([30, 20])}>
          Finish scanning
        </button>
        <button type="button" className="button" onClick={() => setValue(null)}>
          Scan again
        </button>
      </div>
    </div>
  )
}

API reference

10 props across 8 parts, in React syntax. className, style, render are listed once, under Shared props.

IndicatorProgressGroup.Indicator

Draws the segment of one value along the track; no size while indeterminate. Renders a <div>.

Indicator props
PropTypeDefault
indexrequiredThe position in value of the amount this segment draws.number—

ItemProgressGroup.Item

The progress bar of one value, named by its item label: the group's legend entry. Renders a <div>.

Item props
PropTypeDefault
aria-valuetextA string value that provides a user-friendly name for aria-valuenow, the current value ofstring—
indexrequiredThe position in value of the amount this progress bar reads.number—

ItemLabelProgressGroup.ItemLabel

An accessible label for the progress bar. Renders a <span>.

ItemLabel data attributes
Data attributeDescription
data-completePresent when the progress has completed.
data-indeterminatePresent when the progress is in indeterminate state.
data-progressingPresent while the progress is progressing.

ItemValueProgressGroup.ItemValue

A text element displaying the current value. Renders a <span>.

ItemValue props
PropTypeDefault
children((formattedValue: string | null, value: number | null) => React.ReactNode) | null—
ItemValue data attributes
Data attributeDescription
data-completePresent when the progress has completed.
data-indeterminatePresent when the progress is in indeterminate state.
data-progressingPresent while the progress is progressing.

LabelProgressGroup.Label

An accessible label for the group. Renders a <span>.

RootProgressGroup.Root

Groups the progress bars of the parts of one task, named by its label. Renders a <div>.

Root props
PropTypeDefault
formatOptions to format each value and the total.Intl.NumberFormatOptions—
getAriaValueTextA function that returns the aria-valuetext of each item, from its formatted value, its entryProgressGroupGetAriaValueText—
localeThe locale used by Intl.NumberFormat when formatting the values.Intl.LocalesArgument—
maxThe amount the task completes at.number100
valuerequiredThe amounts, in segment order.readonly number[] | null—

TrackProgressGroup.Track

Contains the indicators and represents the whole capacity. Renders a <div>.

ValueProgressGroup.Value

A text element displaying the total of the values. Renders a <span>.

Value props
PropTypeDefault
children((formattedValue: string | null, value: number | null) => React.ReactNode) | null—

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

ProgressGroup takes no focus and has no keys of its own.

Accessibility

What the parts set
PartSetsSo that
Rootrole="group", aria-labelledby to Labelthe stages are read as one named task
Itemrole="progressbar", aria-valuemin="0", aria-valuemax, aria-valuenow (left out while the value is unknown)a screen reader reads how far each stage is, or that it is in progress
Itemaria-valuetext from format, getAriaValueText or aria-valuetext, aria-labelledby to ItemLabeleach stage is read with its name and as written, such as "40%"
Valuethe formatted total as textthe progress of the whole task is read in place
ItemValuearia-hidden="true"the visible number is not read twice

Focus order

  1. ProgressGroup is not focusable; focus passes over it.