# Progress

> Show how much of a task is complete or that work is still in progress.

Source: https://www.honestui.com/docs/components/progress

```tsx
"use client";

import * as React from "react";

import { Button } from "@/components/honest-ui/ui/button";
import {
  Progress,
  ProgressIndicator,
  ProgressLabel,
  ProgressTrack,
  ProgressValue,
} from "@/components/honest-ui/ui/progress";

export function ProgressDemo() {
  const [value, setValue] = React.useState(0);
  const [running, setRunning] = React.useState(false);

  React.useEffect(() => {
    if (!running) return;

    const interval = setInterval(() => {
      setValue((current) => {
        const next = Math.min(100, current + 10);
        if (next === 100) setRunning(false);
        return next;
      });
    }, 400);
    return () => clearInterval(interval);
  }, [running]);

  return (
    <div className="grid w-full max-w-64 gap-[var(--hui-space-4)]">
      <Progress value={value}>
        <div className="flex items-center justify-between gap-[var(--hui-space-3)]">
          <ProgressLabel>Import contacts</ProgressLabel>
          <ProgressValue />
        </div>
        <ProgressTrack>
          <ProgressIndicator />
        </ProgressTrack>
      </Progress>
      <Button
        variant="secondary"
        onClick={() => {
          if (value === 100) setValue(0);
          setRunning(true);
        }}
        disabled={running}
      >
        {running ? "Importing…" : value === 100 ? "Run again" : "Start import"}
      </Button>
    </div>
  );
}

```

## Overview [#overview]

Use Progress to show how far an active task has advanced: uploads, imports, onboarding steps, generation, installation. The defining property of a progress bar is that its endpoint moves — the value climbs while work runs and stops when the task finishes. For quantities that simply *are* a certain amount right now, such as storage used or budget spent, use [Meter](/docs/components/meter) instead.

## Anatomy [#anatomy]

A progress indicator has a root carrying the value semantics, an optional label naming the operation, an optional formatted value, and a visual bar or ring. With no children, `Progress` renders the matching track for its variant automatically; circular layouts also center `ProgressValue` inside the ring for you.

## Behavior [#behavior]

Pass a number as soon as the application can genuinely estimate progress, and pass `value={null}` only when it cannot — an indeterminate bar says "working, duration unknown", which is honest. Switching from indeterminate to determinate mid-task is fine and often ideal: show the spinner-like state during connection, then real percentages once bytes are counted.

Never animate a made-up percentage just so the interface feels alive. A bar that creeps to 90% and sits there teaches people your numbers mean nothing. If the estimate stalls, say so in surrounding text rather than faking motion.

## Accessibility [#accessibility]

The root emits `role="progressbar"` with `aria-valuemin`, `aria-valuemax`, and `aria-valuenow`. Screen readers announce the accessible name plus the current value against the range — for example "Importing contacts, 40%". Give the operation its name through `ProgressLabel` (wired to the root automatically) or `aria-label` when space is tight.

When the raw percentage is unclear, use `getAriaValueText` or `format` to control what gets announced — "2.1 of 4 GB transferred" beats "40" for uploads. Render `ProgressValue` when sighted users also need the number; do not make anyone derive meaning from bar length alone.

Indeterminate animation runs only when motion is safe: under reduced-motion preferences the linear bar becomes a static, dimmed partial fill and the ring holds still, so nothing spins uncontrollably. Colors come from theme tokens for both themes. When the task completes, report it in surrounding content — a status line, a toast, new content appearing — because the bar itself disappears and proves nothing.

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;progress&#x22;]" />
  

  
    
      
        Install the following dependencies:
      

      ```bash
      npm install @base-ui-components/react
      ```

      
        Copy and paste the following code into your project.
      

      ### components/ui/progress.tsx

```tsx
"use client"

import * as React from "react"
import { Progress as ProgressPrimitive } from "@base-ui-components/react/progress"

import { cn } from "@/lib/utils"

function Progress({
  className,
  children,
  max = 100,
  min = 0,
  style,
  value,
  variant = "linear",
  ...props
}: ProgressPrimitive.Root.Props & {
  variant?: "linear" | "circular"
}) {
  const percentage =
    value !== null && Number.isFinite(value) && max > min
      ? Math.min(100, Math.max(0, ((value - min) / (max - min)) * 100))
      : 0

  return (
    <ProgressPrimitive.Root
      data-slot="progress"
      data-variant={variant}
      className={cn(
        "group/progress flex w-full flex-col gap-[var(--hui-space-3)]",
        variant === "circular" && "relative items-center justify-center",
        className
      )}
      max={max}
      min={min}
      style={(state) =>
        ({
          ...(typeof style === "function" ? style(state) : style),
          "--hui-progress-percentage": percentage,
        }) as React.CSSProperties & { "--hui-progress-percentage": number }
      }
      value={value}
      {...props}
    >
      {children ? (
        children
      ) : variant === "circular" ? (
        <>
          <ProgressCircularTrack />
          <ProgressValue />
        </>
      ) : (
        <ProgressTrack>
          <ProgressIndicator />
        </ProgressTrack>
      )}
    </ProgressPrimitive.Root>
  )
}

function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) {
  return (
    <ProgressPrimitive.Label
      data-slot="progress-label"
      className={cn(
        "text-[var(--hui-color-foreground-base-primary)] [font-family:var(--hui-font-body)] [font-size:var(--hui-font-size-mini)] [font-weight:var(--hui-font-weight-medium)] [letter-spacing:var(--hui-letter-spacing-mini)] [line-height:var(--hui-line-height-mini)]",
        className
      )}
      {...props}
    />
  )
}

function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) {
  return (
    <ProgressPrimitive.Track
      data-slot="progress-track"
      className={cn(
        "relative block h-[var(--hui-space-2)] w-full overflow-clip rounded-[1px] bg-[var(--hui-color-background-neutral-secondary)]",
        className
      )}
      {...props}
    />
  )
}

function ProgressIndicator({
  className,
  style,
  ...props
}: ProgressPrimitive.Indicator.Props) {
  return (
    <ProgressPrimitive.Indicator
      data-slot="progress-indicator"
      className={cn(
        "h-full origin-left bg-[var(--hui-color-background-accent-emphasis)] [transform:scaleX(calc(var(--hui-progress-percentage,0)/100))] data-indeterminate:origin-center data-indeterminate:opacity-60 data-indeterminate:[transform:scaleX(0.4)] motion-safe:[transition:transform_var(--hui-duration-moderate)_linear] motion-safe:data-indeterminate:origin-left motion-safe:data-indeterminate:opacity-100 motion-safe:data-indeterminate:[animation:progress-indeterminate-sweep_1.2s_var(--hui-ease-in-out)_infinite] motion-safe:data-indeterminate:[transition:none]",
        className
      )}
      style={(state) => ({
        ...(typeof style === "function" ? style(state) : style),
        width: "100%",
      })}
      {...props}
    />
  )
}

function ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props) {
  return (
    <ProgressPrimitive.Value
      data-slot="progress-value"
      className={cn(
        "text-right text-[var(--hui-color-foreground-base-primary)] tabular-nums [font-family:var(--hui-font-body)] [font-size:var(--hui-font-size-mini)] [font-weight:var(--hui-font-weight-regular)] [letter-spacing:var(--hui-letter-spacing-mini)] [line-height:var(--hui-line-height-mini)] group-data-[variant=circular]/progress:absolute group-data-[variant=circular]/progress:top-1/2 group-data-[variant=circular]/progress:left-1/2 group-data-[variant=circular]/progress:-translate-x-1/2 group-data-[variant=circular]/progress:-translate-y-1/2 group-data-[variant=circular]/progress:whitespace-nowrap group-data-[variant=circular]/progress:text-center group-data-[variant=circular]/progress:[font-weight:var(--hui-font-weight-medium)]",
        className
      )}
      {...props}
    />
  )
}

function ProgressCircularTrack({
  className,
  ...props
}: React.ComponentProps<"svg">) {
  return (
    <svg
      aria-hidden="true"
      data-slot="progress-circular-track"
      viewBox="0 0 72 72"
      className={cn(
        "aspect-square h-[var(--hui-space-14)] w-[var(--hui-space-14)] -rotate-90 [--hui-progress-circumference:calc(2*3.14159265*var(--hui-progress-radius))] [--hui-progress-radius:calc((var(--hui-space-14)-var(--hui-progress-track-size)*2)/2)] [--hui-progress-track-size:var(--hui-space-2)] motion-safe:group-data-[indeterminate]/progress:[animation:progress-indeterminate-rotate_1.2s_linear_infinite]",
        className
      )}
      {...props}
    >
      <circle
        data-slot="progress-circular-track-circle"
        className="fill-none stroke-[var(--hui-color-background-neutral-secondary)] [cx:50%] [cy:50%] [r:var(--hui-progress-radius)] [stroke-width:var(--hui-progress-track-size)]"
      />
      <circle
        data-slot="progress-circular-indicator-circle"
        className="fill-none stroke-[var(--hui-color-background-accent-emphasis)] [cx:50%] [cy:50%] [r:var(--hui-progress-radius)] [stroke-dasharray:var(--hui-progress-circumference)] [stroke-dashoffset:calc(var(--hui-progress-circumference)*(1-var(--hui-progress-percentage,0)/100))] [stroke-linecap:butt] [stroke-width:var(--hui-progress-track-size)] group-data-[indeterminate]/progress:opacity-60 group-data-[indeterminate]/progress:[stroke-dashoffset:calc(var(--hui-progress-circumference)*0.75)] motion-safe:[transition:stroke-dashoffset_var(--hui-duration-moderate)_linear] motion-safe:group-data-[indeterminate]/progress:opacity-100"
      />
    </svg>
  )
}

export {
  Progress,
  ProgressLabel,
  ProgressTrack,
  ProgressIndicator,
  ProgressValue,
  ProgressCircularTrack,
}

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

```tsx
import {
  Progress,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress";
```

```tsx
<Progress value={40} />
```

Note: If you render children inside `Progress`, you must also include `ProgressTrack` and `ProgressIndicator` inside it. Without them, the bar will not display. When no children are provided, a default track and indicator are rendered for you.

## Don't do this [#dont-do-this]

### Fake progress [#fake-progress]

```tsx
// Bad
const [value, setValue] = useState(0);
useEffect(() => {
  const t = setInterval(() => setValue((v) => Math.min(v + 5, 95)), 500);
  return () => clearInterval(t);
}, []);
return <Progress value={value} />;
```

```tsx
// Good
<Progress value={uploadProgress ?? null} />
// uploadProgress comes from real transfer events; null while unknown
```

An invented ticker produces the worst possible outcome: a bar that reaches 90% on a task that then fails, or completes instantly after crawling. People learn to ignore the component entirely. Use `value={null}` while the outcome is genuinely unknown and switch to real numbers when events arrive.

### An unnamed bar [#an-unnamed-bar]

```tsx
// Bad
<Progress value={40} />
```

```tsx
// Good
<Progress value={40}>
  <div className="flex justify-between">
    <ProgressLabel>Importing contacts</ProgressLabel>
    <ProgressValue />
  </div>
  <ProgressTrack>
    <ProgressIndicator />
  </ProgressTrack>
</Progress>
```

Without a label, assistive technology announces "40%" with no idea what it belongs to — on a page with two operations running, that announces nothing useful. Name every progress bar, even single ones; screens change.

### Progress where Meter belongs [#progress-where-meter-belongs]

```tsx
// Bad
<Progress value={72}>
  <ProgressLabel>Storage used</ProgressLabel>
</Progress>
```

```tsx
// Good
<Meter value={72}>
  <MeterLabel>Storage used</MeterLabel>
  <MeterValue />
</Meter>
```

Storage does not advance toward completion; it sits at a measured level within a capacity. Announcing `role="progressbar"` invites screen-reader users to wait for a finish that never comes. Reserve Progress for tasks that end.

## Examples [#examples]

### Onboarding steps [#onboarding-steps]

A labeled determinate bar tracking multi-step setup.

```tsx
import { Progress, ProgressIndicator, ProgressTrack, ProgressValue } from "@/components/honest-ui/ui/progress"

export function ProgressOnboarding() {
  return (
    <Progress value={66} className="w-full max-w-xs">
      <div className="flex justify-between text-sm font-medium"><span>Onboarding</span><ProgressValue /></div>
      <ProgressTrack><ProgressIndicator /></ProgressTrack>
    </Progress>
  )
}

```

### With Label and Value [#with-label-and-value]

```tsx
import {
  Progress,
  ProgressIndicator,
  ProgressLabel,
  ProgressTrack,
  ProgressValue,
} from "@/components/honest-ui/ui/progress";

export function ProgressWithLabelValueDemo() {
  return (
    <div className="grid w-full max-w-sm gap-6">
      <Progress value={60}>
        <div className="flex items-center justify-between gap-2">
          <ProgressLabel>Export data</ProgressLabel>
          <ProgressValue />
        </div>
        <ProgressTrack>
          <ProgressIndicator />
        </ProgressTrack>
      </Progress>
      <div className="flex items-center gap-8">
        <Progress aria-label="Export data" value={60} variant="circular" />
        <Progress
          aria-label="Preparing export"
          value={null}
          variant="circular"
        />
      </div>
    </div>
  );
}

```

### With Formatted Value [#with-formatted-value]

Custom units and precision via the formatting props.

```tsx
"use client"

import {
  Progress,
  ProgressIndicator,
  ProgressLabel,
  ProgressTrack,
  ProgressValue,
} from "@/components/honest-ui/ui/progress"

export function ProgressWithFormattedValueDemo() {
  return (
    <Progress className="w-full max-w-64" value={502} max={512}>
      <div className="flex items-center justify-between gap-2">
        <ProgressLabel>Upload</ProgressLabel>
        <ProgressValue>{(_formatted, value) => `${value} / 512`}</ProgressValue>
      </div>
      <ProgressTrack>
        <ProgressIndicator />
      </ProgressTrack>
    </Progress>
  )
}

```

## API reference [#api-reference]

`Progress` accepts all Base UI Progress root props plus Honest UI's `variant`:

| Prop          | Values                           | Default     |
| ------------- | -------------------------------- | ----------- |
| `variant`     | `linear`, `circular`             | `linear`    |
| `value`       | number or `null` (indeterminate) | required    |
| `min` / `max` | number                           | `0` / `100` |

The root carries `role="progressbar"` with `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, and `aria-valuetext`; `format` (Intl options), `locale`, and `getAriaValueText(formattedValue, value)` control the announced text. Current status (`indeterminate`, `progressing`, `complete`) is exposed as data attributes for styling. Computed percentages are clamped to 0–100.

Parts: `ProgressLabel`, `ProgressTrack`, `ProgressIndicator`, and `ProgressValue` forward their Base UI props; `ProgressCircularTrack` accepts native SVG props.

See the [Base UI Progress API](https://base-ui.com/react/components/progress#api-reference) for value formatting and state details.
