Feedback

Progress

A labelled bar showing how far a task has got, or that it is under way when the end is unknown.

Built on
<progress>:indeterminate::-webkit-progress-value::-moz-progress-bar
Client JavaScript
None
Works without React
Yes, as HTML and CSS
Source, gzipped
1.5 kB
Dependencies
React
64%
Try its options

Loading its options.

Code
import { Progress } from "@/components/equip/progress";

export default function Demo() {
  return (
    <div style={{ display: "grid", gap: 16, width: 208 }}>
      <Progress label="Uploading brand-kit.zip" value={64} valueText="64%" />
      <Progress label="Restoring backup" />
    </div>
  );
}
Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:16px;width:208px">
  <div class="eq-progress"><label><span class="eq-progress-label">Uploading brand-kit.zip</span><progress aria-valuetext="64%" class="eq-progress-bar" max="100" value="64"></progress></label><span class="eq-progress-value">64%</span></div>
  <div class="eq-progress">
    <label><span class="eq-progress-label">Restoring backup</span><progress class="eq-progress-bar" max="100"></progress></label>
  </div>
</div>

Examples

Counts instead of percent

A string valueText is also set as aria-valuetext, so screen readers hear "12 of 40 files" rather than 30 percent.

12 of 40 files
7.4 of 10 GB
Code
import { Progress } from "@/components/equip/progress";

export default function Example() {
  return (
    <div style={{ display: "grid", gap: 16, maxWidth: 320 }}>
      <Progress label="Syncing photos" value={12} max={40} valueText="12 of 40 files" />
      <Progress label="Storage used" value={7.4} max={10} valueText="7.4 of 10 GB" />
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:16px;max-width:320px">
  <div class="eq-progress"><label><span class="eq-progress-label">Syncing photos</span><progress aria-valuetext="12 of 40 files" class="eq-progress-bar" max="40" value="12"></progress></label><span class="eq-progress-value">12 of 40 files</span></div>
  <div class="eq-progress"><label><span class="eq-progress-label">Storage used</span><progress aria-valuetext="7.4 of 10 GB" class="eq-progress-bar" max="10" value="7.4"></progress></label><span class="eq-progress-value">7.4 of 10 GB</span></div>
</div>

Empty, partway, done and indeterminate

Leave out value while the size of the job is unknown; value={0} is still a determinate bar.

0%
38%
Done
Code
import { Progress } from "@/components/equip/progress";

export default function Example() {
  return (
    <div style={{ display: "grid", gap: 16, maxWidth: 320 }}>
      <Progress label="Queued: migrate users table" value={0} valueText="0%" />
      <Progress label="Building larder-web" value={38} valueText="38%" />
      <Progress label="Tests passed" value={100} valueText="Done" />
      <Progress label="Waiting for a runner" />
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:16px;max-width:320px">
  <div class="eq-progress"><label><span class="eq-progress-label">Queued: migrate users table</span><progress aria-valuetext="0%" class="eq-progress-bar" max="100" value="0"></progress></label><span class="eq-progress-value">0%</span></div>
  <div class="eq-progress"><label><span class="eq-progress-label">Building larder-web</span><progress aria-valuetext="38%" class="eq-progress-bar" max="100" value="38"></progress></label><span class="eq-progress-value">38%</span></div>
  <div class="eq-progress"><label><span class="eq-progress-label">Tests passed</span><progress aria-valuetext="Done" class="eq-progress-bar" max="100" value="100"></progress></label><span class="eq-progress-value">Done</span></div>
  <div class="eq-progress">
    <label><span class="eq-progress-label">Waiting for a runner</span><progress class="eq-progress-bar" max="100"></progress></label>
  </div>
</div>

Props

PropTypeDefaultWhat it does
labelReactNodenoneWhat is in progress. Wraps the bar in a label, so it becomes the accessible name.
valuenumbernoneHow far along, from 0 to max. Leave out for an indeterminate bar.
maxnumber100The value at which the task is complete.
valueTextReactNodenoneShown opposite the label. A string is also used as aria-valuetext on a determinate bar.
classNamestringnoneAdded to the root wrapper, so it sizes and places the whole part.
...restComponentProps<"progress">noneEvery other native progress attribute, including id, ref, style and aria-describedby, goes on the bar itself.

Anatomy

root.eq-progressA two-row grid: label and value text, then the bar.
label.eq-progress-label
bar.eq-progress-barThe native progress element.
value.eq-progress-valueOutside the label, so it is not read as part of the name.

Notes

  • Renders on the server with no client JavaScript; update value from wherever the work is tracked.
  • The indeterminate bar animates on its own, as a progress indicator may; under reduced motion it slides at a slow, steady pace.
  • Progress is not a live region. If finishing matters, say so in an Alert or other status message as well.

Source

Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.

components/equip/progress.tsx30 lines
import type { ComponentProps, ReactNode } from "react";
import "./progress.css";

export type ProgressProps = Omit<ComponentProps<"progress">, "value"> & {
  /** What is in progress. Wraps the bar in its label, so it is the bar's accessible name. */
  label: ReactNode;
  /** Leave out for an indeterminate bar that only says work is under way. */
  value?: number;
  /** Shown opposite the label, such as "64%" or "12 of 40 files". A string is also used as aria-valuetext. */
  valueText?: ReactNode;
};

export function Progress({ label, value, valueText, max = 100, className, ...rest }: ProgressProps) {
  return (
    <div className={["eq-progress", className].filter(Boolean).join(" ")}>
      {/* display: contents, so the label's text and bar sit in the root's grid. */}
      <label>
        <span className="eq-progress-label">{label}</span>
        <progress
          aria-valuetext={typeof valueText === "string" && value !== undefined ? valueText : undefined}
          {...rest}
          className="eq-progress-bar"
          max={max}
          value={value}
        />
      </label>
      {valueText != null && <span className="eq-progress-value">{valueText}</span>}
    </div>
  );
}
components/equip/progress.css94 lines
@layer equip {
  .eq-progress {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: baseline;
    gap: var(--eq-space-1) var(--eq-space-3);
    font: var(--eq-weight) var(--eq-text-s) / 1.4 var(--eq-font);
  }

  /* The label element only ties the text to the bar; its children join the grid above. */
  .eq-progress > label {
    display: contents;
  }

  .eq-progress-label {
    grid-area: 1 / 1;
    min-width: 0;
    color: var(--eq-ink);
    font-weight: var(--eq-weight-strong);
  }

  .eq-progress-value {
    grid-area: 1 / 2;
    color: var(--eq-ink-2);
    font-variant-numeric: tabular-nums;
  }

  /* The element's own background is the track in every engine. */
  .eq-progress-bar {
    grid-area: 2 / 1 / 3 / 3;
    display: block;
    width: 100%;
    height: 6px;
    margin: 0;
    border: 0;
    border-radius: var(--eq-radius-full);
    overflow: hidden;
    appearance: none;
    background: var(--eq-tint-strong);
    color: var(--eq-solid);
  }

  /* WebKit and Blink: the bar pseudo covers the element, so it stays clear. */
  .eq-progress-bar::-webkit-progress-bar {
    background: transparent;
  }

  .eq-progress-bar::-webkit-progress-value {
    border-radius: var(--eq-radius-full);
    background: var(--eq-solid);
    /* A measurement: it travels, but never overshoots the value it reports. */
    transition: width var(--eq-dur-move) var(--eq-ease);
  }

  /* Firefox. Kept in its own rule: an unknown pseudo-element would void a shared one. */
  .eq-progress-bar::-moz-progress-bar {
    border-radius: var(--eq-radius-full);
    background: var(--eq-solid);
  }

  /* Indeterminate: both engines' fills are cleared and a segment is drawn on the track. */
  .eq-progress-bar:indeterminate {
    background:
      linear-gradient(var(--eq-solid), var(--eq-solid)) no-repeat,
      var(--eq-tint-strong);
    background-size: 40% 100%;
    animation: eq-progress-slide 1.4s cubic-bezier(0.65, 0, 0.35, 1) infinite;
  }

  .eq-progress-bar:indeterminate::-webkit-progress-value {
    background: transparent;
  }

  .eq-progress-bar:indeterminate::-moz-progress-bar {
    background: transparent;
  }

  /* With a 40% segment, -70% and 170% put it just past each end. */
  @keyframes eq-progress-slide {
    from {
      background-position: -70% 0;
    }
    to {
      background-position: 170% 0;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .eq-progress-bar:indeterminate {
      animation-duration: 4s;
      animation-timing-function: linear;
    }
  }
}

EqUIp is in beta: free for non-commercial use, no warranty, and things can change. Terms

Nothing picked yet. Press A on a part, or use the plus on the board, and the install command builds here.

Loading the index.

Loading the index