# Progress

> A bar or ring that shows how far a task has come, eases between updates and slides while the total is unknown.

Docs: https://hextaui.com/docs/progress
Markdown: https://hextaui.com/docs/progress.md

```tsx title="components/examples/progress/demo.tsx"
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  Progress,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"

export function ProgressDemo() {
  const [value, setValue] = React.useState<number | null>(null)
  const [run, setRun] = React.useState(0)

  React.useEffect(() => {
    let current = 0
    let tick: ReturnType<typeof setInterval> | undefined
    const start = setTimeout(() => {
      setValue(0)
      tick = setInterval(() => {
        current = Math.min(100, current + Math.round(Math.random() * 12 + 3))
        setValue(current)
        if (current === 100) {
          clearInterval(tick)
        }
      }, 400)
    }, 1200)
    return () => {
      clearTimeout(start)
      clearInterval(tick)
    }
  }, [run])

  const label =
    value === null ? "Preparing…" : value === 100 ? "Uploaded" : "Uploading"

  return (
    <div className="flex w-full max-w-sm flex-col items-center gap-6">
      <Progress value={value}>
        <ProgressLabel>{label}</ProgressLabel>
        <ProgressValue />
      </Progress>
      <Button
        variant="outline"
        size="sm"
        onClick={() => {
          setValue(null)
          setRun(run + 1)
        }}
      >
        Restart
      </Button>
    </div>
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/progress.json
```

This adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @base-ui/react class-variance-authority cn
```

Copy and paste the following code into your project.

```tsx title="components/ui/progress.tsx"
"use client"

import * as React from "react"
import { Progress as ProgressPrimitive } from "@base-ui/react/progress"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"

type ClassName<State> =
  string | ((state: State) => string | undefined) | undefined

function mergeClassName<State>(base: string, className: ClassName<State>) {
  return typeof className === "function"
    ? (state: State) => cn(base, className(state))
    : cn(base, className)
}

type ProgressSize = "xs" | "sm" | "default" | "lg"

type ProgressVariant = "default" | "success" | "warning" | "destructive"

type ProgressCircleSize = "sm" | "default" | "lg" | "xl"

const ProgressContext = React.createContext<{
  kind: "bar" | "circle"
  size: ProgressSize | ProgressCircleSize
  variant: ProgressVariant
} | null>(null)

function useProgressContext(part: string) {
  const context = React.useContext(ProgressContext)
  if (!context) {
    throw new Error(`${part} must be used within <Progress>.`)
  }
  return context
}

function toPercent(value: number | null | undefined, min: number, max: number) {
  if (value == null || !Number.isFinite(value)) {
    return null
  }
  const percent = ((value - min) * 100) / (max - min)
  return Number.isNaN(percent) ? 0 : Math.min(100, Math.max(0, percent))
}

const progressTrackVariants = cva(
  "relative flex w-full items-center overflow-hidden rounded-full bg-foreground/10 [--progress-dir:1] rtl:[--progress-dir:-1] dark:bg-foreground/15 forced-colors:border",
  {
    variants: {
      size: {
        xs: "h-0.5",
        sm: "h-1",
        default: "h-1.5",
        lg: "h-2.5",
      },
    },
    defaultVariants: {
      size: "default",
    },
  }
)

const progressIndicatorVariants = cva(
  "h-full w-0 rounded-full transition-[width] duration-300 ease-spring data-indeterminate:transition-none data-indeterminate:before:absolute data-indeterminate:before:inset-y-0 data-indeterminate:before:start-0 data-indeterminate:before:w-2/5 data-indeterminate:before:rounded-full data-indeterminate:before:bg-inherit motion-safe:data-indeterminate:before:animate-progress-slide motion-reduce:transition-none motion-reduce:data-indeterminate:before:w-full motion-reduce:data-indeterminate:before:animate-skeleton-pulse forced-colors:bg-highlight",
  {
    variants: {
      variant: {
        default: "bg-primary",
        success: "bg-success",
        warning: "bg-warning",
        destructive: "bg-destructive",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

const progressCircleStroke = {
  default: "stroke-primary",
  success: "stroke-success",
  warning: "stroke-warning",
  destructive: "stroke-destructive",
} satisfies Record<ProgressVariant, string>

const progressCircleVariants = cva(
  "relative inline-grid size-(--progress-circle-size) shrink-0 place-items-center *:col-start-1 *:row-start-1",
  {
    variants: {
      size: {
        sm: "text-[0.5rem] [--progress-circle-size:1rem] [--progress-stroke:3px]",
        default:
          "text-[0.5rem] [--progress-circle-size:1.5rem] [--progress-stroke:2.5px]",
        lg: "text-[0.625rem] [--progress-circle-size:2.5rem] [--progress-stroke:2px]",
        xl: "text-sm [--progress-circle-size:4rem] [--progress-stroke:1.5px]",
      },
    },
    defaultVariants: {
      size: "default",
    },
  }
)

type ProgressProps = ProgressPrimitive.Root.Props & {
  size?: ProgressSize
  variant?: ProgressVariant
}

function Progress({
  className,
  children,
  size = "default",
  variant = "default",
  locale = "en-US",
  ...props
}: ProgressProps) {
  const context = React.useMemo(
    () => ({ kind: "bar" as const, size, variant }),
    [size, variant]
  )

  return (
    <ProgressContext.Provider value={context}>
      <ProgressPrimitive.Root
        data-slot="progress"
        data-size={size}
        data-variant={variant}
        locale={locale}
        className={mergeClassName(
          "flex w-full min-w-0 flex-wrap items-center gap-x-3 gap-y-2",
          className
        )}
        {...props}
      >
        {children}
        <ProgressTrack>
          <ProgressIndicator />
        </ProgressTrack>
      </ProgressPrimitive.Root>
    </ProgressContext.Provider>
  )
}

function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) {
  const { size } = useProgressContext("ProgressTrack")

  return (
    <ProgressPrimitive.Track
      data-slot="progress-track"
      className={mergeClassName(
        progressTrackVariants({ size: size === "xl" ? "lg" : size }),
        className
      )}
      {...props}
    />
  )
}

function ProgressIndicator({
  className,
  ...props
}: ProgressPrimitive.Indicator.Props) {
  const { variant } = useProgressContext("ProgressIndicator")

  return (
    <ProgressPrimitive.Indicator
      data-slot="progress-indicator"
      className={mergeClassName(
        progressIndicatorVariants({ variant }),
        className
      )}
      {...props}
    />
  )
}

function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) {
  return (
    <ProgressPrimitive.Label
      data-slot="progress-label"
      className={mergeClassName(
        "min-w-0 flex-1 text-sm font-medium wrap-anywhere",
        className
      )}
      {...props}
    />
  )
}

function ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props) {
  const { kind } = useProgressContext("ProgressValue")

  return (
    <ProgressPrimitive.Value
      data-slot="progress-value"
      className={mergeClassName(
        kind === "bar"
          ? "ms-auto shrink-0 text-sm text-muted-foreground tabular-nums"
          : "font-medium tabular-nums",
        className
      )}
      {...props}
    />
  )
}

type ProgressCircleProps = ProgressPrimitive.Root.Props &
  VariantProps<typeof progressCircleVariants> & {
    variant?: ProgressVariant
  }

function ProgressCircle({
  className,
  children,
  size = "default",
  variant = "default",
  value,
  min = 0,
  max = 100,
  locale = "en-US",
  ...props
}: ProgressCircleProps) {
  const resolvedSize = size ?? "default"
  const context = React.useMemo(
    () => ({ kind: "circle" as const, size: resolvedSize, variant }),
    [resolvedSize, variant]
  )
  const percent = toPercent(value, min, max)

  return (
    <ProgressContext.Provider value={context}>
      <ProgressPrimitive.Root
        data-slot="progress-circle"
        data-size={resolvedSize}
        data-variant={variant}
        value={value}
        min={min}
        max={max}
        locale={locale}
        className={mergeClassName(
          progressCircleVariants({ size: resolvedSize }),
          className
        )}
        {...props}
      >
        <svg
          viewBox="0 0 24 24"
          fill="none"
          aria-hidden
          data-slot="progress-circle-svg"
          style={{ "--progress": percent ?? 0 } as React.CSSProperties}
          className="size-full -rotate-90"
        >
          <circle
            cx="12"
            cy="12"
            r="10"
            data-slot="progress-circle-track"
            className="stroke-foreground/10 [stroke-width:var(--progress-stroke)] dark:stroke-foreground/15"
          />
          <circle
            cx="12"
            cy="12"
            r="10"
            pathLength={100}
            data-slot="progress-circle-indicator"
            className={cn(
              "origin-center [stroke-width:var(--progress-stroke)] [stroke-dasharray:100_100] [stroke-linecap:round] [transform-box:fill-box]",
              progressCircleStroke[variant],
              percent === null
                ? "[stroke-dasharray:25_75] motion-safe:animate-spinner-rotate motion-reduce:animate-skeleton-pulse motion-reduce:[stroke-dasharray:100_100]"
                : "transition-[stroke-dashoffset,opacity] duration-300 ease-spring [stroke-dashoffset:calc(100_-_var(--progress))] motion-reduce:transition-none",
              percent === 0 && "opacity-0"
            )}
          />
        </svg>
        {children}
      </ProgressPrimitive.Root>
    </ProgressContext.Provider>
  )
}

export {
  Progress,
  ProgressCircle,
  progressCircleVariants,
  ProgressIndicator,
  progressIndicatorVariants,
  ProgressLabel,
  ProgressTrack,
  progressTrackVariants,
  ProgressValue,
}
export type {
  ProgressCircleProps,
  ProgressCircleSize,
  ProgressProps,
  ProgressSize,
  ProgressVariant,
}
```

Update the import paths to match your project setup.

## Usage

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

```tsx
<Progress value={40}>
  <ProgressLabel>Uploading</ProgressLabel>
  <ProgressValue />
</Progress>

<ProgressCircle value={40} aria-label="Uploading" />
```

`<Progress />` draws its own track and indicator after its children, so a label and value sit on one line above the bar. Each update eases the fill from where it is, so rapid updates read as one smooth motion instead of steps.

## Composition

```text
Progress
├── ProgressLabel
└── ProgressValue

ProgressCircle
└── ProgressValue
```

## Examples

### Sizes

`xs`, `sm`, `default` and `lg` change the bar's thickness. `xs` is the hairline Attachment draws along its bottom edge.

```tsx title="components/examples/progress/sizes.tsx"
import {
  Progress,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"

export function ProgressSizes() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-6">
      <Progress value={15} size="xs">
        <ProgressLabel>Extra small</ProgressLabel>
        <ProgressValue />
      </Progress>
      <Progress value={30} size="sm">
        <ProgressLabel>Small</ProgressLabel>
        <ProgressValue />
      </Progress>
      <Progress value={55}>
        <ProgressLabel>Default</ProgressLabel>
        <ProgressValue />
      </Progress>
      <Progress value={80} size="lg">
        <ProgressLabel>Large</ProgressLabel>
        <ProgressValue />
      </Progress>
    </div>
  )
}
```

### Status

`variant` colors only the fill or ring, so the track, label and value stay neutral.

```tsx title="components/examples/progress/variants.tsx"
import {
  Progress,
  ProgressCircle,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"

export function ProgressVariants() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-6">
      <Progress value={100} variant="success">
        <ProgressLabel>Backup complete</ProgressLabel>
        <ProgressValue />
      </Progress>
      <Progress value={86} variant="warning">
        <ProgressLabel>Storage almost full</ProgressLabel>
        <ProgressValue />
      </Progress>
      <Progress value={47} variant="destructive">
        <ProgressLabel>Upload failed</ProgressLabel>
        <ProgressValue />
      </Progress>
      <div className="flex items-center gap-4">
        <ProgressCircle value={100} variant="success" aria-label="Synced" />
        <ProgressCircle value={86} variant="warning" aria-label="Almost full" />
        <ProgressCircle value={47} variant="destructive" aria-label="Failed" />
      </div>
    </div>
  )
}
```

### Indeterminate

Pass `value={null}` while the total is unknown. A segment slides across the track, and once a number arrives the fill grows from the start.

```tsx title="components/examples/progress/indeterminate.tsx"
import { Progress, ProgressLabel } from "@/components/ui/progress"

export function ProgressIndeterminate() {
  return (
    <div className="w-full max-w-sm">
      <Progress value={null}>
        <ProgressLabel>Connecting to server…</ProgressLabel>
      </Progress>
    </div>
  )
}
```

### Circle

`<ProgressCircle />` draws the same value as a ring, starting at the top. Children sit in the middle, which fits `<ProgressValue />` at `lg` and `xl`.

```tsx title="components/examples/progress/circle.tsx"
import { ProgressCircle, ProgressValue } from "@/components/ui/progress"

export function ProgressCircleDemo() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-8">
      <ProgressCircle value={25} size="sm" aria-label="Small" />
      <ProgressCircle value={50} aria-label="Default" />
      <ProgressCircle value={75} size="lg" aria-label="Large">
        <ProgressValue />
      </ProgressCircle>
      <ProgressCircle value={100} size="xl" aria-label="Extra large">
        <ProgressValue />
      </ProgressCircle>
    </div>
  )
}
```

### Indeterminate circle

An arc spins around the ring until a value arrives.

```tsx title="components/examples/progress/circle-indeterminate.tsx"
import { ProgressCircle } from "@/components/ui/progress"

export function ProgressCircleIndeterminate() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-8">
      <ProgressCircle value={null} size="sm" aria-label="Syncing" />
      <ProgressCircle value={null} aria-label="Syncing" />
      <ProgressCircle value={null} size="lg" aria-label="Syncing" />
      <ProgressCircle value={null} size="xl" aria-label="Syncing" />
    </div>
  )
}
```

### Custom range and format

Set `min` and `max` for any range, `format` for the number, and a function child on `<ProgressValue />` for the text. Give screen readers the same words with `getAriaValueText`.

```tsx title="components/examples/progress/format.tsx"
"use client"

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

export function ProgressFormat() {
  return (
    <div className="w-full max-w-sm">
      <Progress
        value={37.5}
        max={50}
        format={{ maximumFractionDigits: 1 }}
        getAriaValueText={(formatted) => `${formatted} of 50 GB used`}
      >
        <ProgressLabel>Storage</ProgressLabel>
        <ProgressValue>{(formatted) => `${formatted} of 50 GB`}</ProgressValue>
      </Progress>
    </div>
  )
}
```

### Animated value

Render `<NumberFlow />` inside `<ProgressValue />` so only the digits that change spin, in step with the fill.

```tsx title="components/examples/progress/number-flow.tsx"
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { NumberFlow } from "@/components/ui/number-flow"
import {
  Progress,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"

export function ProgressNumberFlow() {
  const [value, setValue] = React.useState(42)

  return (
    <div className="flex w-full max-w-sm flex-col items-center gap-6">
      <Progress value={value}>
        <ProgressLabel>Course completed</ProgressLabel>
        <ProgressValue>
          {(_, current) => <NumberFlow value={current ?? 0} suffix="%" />}
        </ProgressValue>
      </Progress>
      <div className="flex gap-2">
        <Button
          variant="outline"
          size="sm"
          onClick={() => setValue(Math.max(0, value - 13))}
        >
          −13
        </Button>
        <Button
          variant="outline"
          size="sm"
          onClick={() => setValue(Math.min(100, value + 13))}
        >
          +13
        </Button>
      </div>
    </div>
  )
}
```

### Long labels

Long names wrap onto their own lines and the value stays on the end. Rings work as compact status beside each row.

```tsx title="components/examples/progress/files.tsx"
import {
  Progress,
  ProgressCircle,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"

const files = [
  {
    name: "quarterly-report-final-v3-approved-by-legal-and-finance.pdf",
    value: 64,
  },
  { name: "IMG_20260914_183022_HDR_edited_export.jpg", value: 100 },
  { name: "brand-assets.zip", value: 12 },
]

export function ProgressFiles() {
  return (
    <ul className="flex w-full max-w-sm flex-col gap-5">
      {files.map((file) => (
        <li key={file.name} className="flex items-start gap-3">
          <ProgressCircle value={file.value} aria-label={file.name} />
          <div className="min-w-0 flex-1">
            <Progress value={file.value} size="sm">
              <ProgressLabel>{file.name}</ProgressLabel>
              <ProgressValue />
            </Progress>
          </div>
        </li>
      ))}
    </ul>
  )
}
```

### Without a visible label

Name the bar with `aria-label` when the context already says what is loading.

```tsx title="components/examples/progress/unlabeled.tsx"
import { Progress } from "@/components/ui/progress"

export function ProgressUnlabeled() {
  return (
    <div className="w-full max-w-sm">
      <Progress value={45} aria-label="Profile setup" />
    </div>
  )
}
```

### Right to left

The fill and the indeterminate slide start from the right. Pass `locale` to format the value in the reader's digits.

```tsx title="components/examples/progress/rtl.tsx"
import {
  Progress,
  ProgressCircle,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"

export function ProgressRtl() {
  return (
    <div dir="rtl" className="flex w-full max-w-sm flex-col gap-6">
      <Progress value={65} locale="ar-EG">
        <ProgressLabel>جارٍ التحميل</ProgressLabel>
        <ProgressValue />
      </Progress>
      <Progress value={null}>
        <ProgressLabel>جارٍ الاتصال…</ProgressLabel>
      </Progress>
      <ProgressCircle value={65} size="xl" locale="ar-EG" aria-label="التقدم">
        <ProgressValue />
      </ProgressCircle>
    </div>
  )
}
```

## Accessibility

- The root is a `progressbar` with `aria-valuenow`, `aria-valuemin`, `aria-valuemax` and a formatted `aria-valuetext`. While indeterminate it has no current value.
- `<ProgressLabel />` names the bar. Without one, pass `aria-label`.
- `<ProgressValue />` is hidden from screen readers, since the progressbar already announces the value.
- With reduced motion, the fill jumps to each new value, and the indeterminate bar and ring pulse in place instead of moving.
- Values are formatted in `en-US` unless you pass `locale`, so the server and browser render the same text.

## API reference

Built on the Base UI progress. Every part accepts the props of the primitive it wraps.

### Progress

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `number \| null` | – | null makes the bar indeterminate. |
| `min` | `number` | `0` |  |
| `max` | `number` | `100` |  |
| `size` | `"xs" \| "sm" \| "default" \| "lg"` | `"default"` |  |
| `variant` | `"default" \| "success" \| "warning" \| "destructive"` | `"default"` |  |
| `format` | `Intl.NumberFormatOptions` | – | Formats the value. Without it, the value shows as a percentage. |
| `locale` | `Intl.LocalesArgument` | `"en-US"` |  |
| `getAriaValueText` | `(formattedValue: string, value: number \| null) => string` | – |  |
| `className` | `string \| (state) => string` | – |  |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="progress"` | The root. |
| `data-size` | The size: xs, sm, default or lg. |
| `data-variant` | The status variant. |
| `data-progressing` | Present while the value is below max. |
| `data-complete` | Present when the value reaches max. |
| `data-indeterminate` | Present when the value is null or not a finite number. |

### ProgressLabel

Names the progressbar. Renders a `<span>` and takes the same state attributes as the root.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<span>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="progress-label"` | The label. |

### ProgressValue

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `(formattedValue: string \| null, value: number \| null) => ReactNode` | – | Custom text. Without it, the formatted value shows, or nothing while indeterminate. |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<span>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="progress-value"` | The value. |

### ProgressTrack

Rendered by `<Progress />` and sized by its `size`. Exported for custom compositions.

| Attribute | Description |
| --- | --- |
| `data-slot="progress-track"` | The track. |
| `--progress-dir` | 1, or -1 in right-to-left, so the indeterminate slide follows the reading direction. |

### ProgressIndicator

The fill. Its width is set inline from the value and eases between updates.

| Attribute | Description |
| --- | --- |
| `data-slot="progress-indicator"` | The fill. |

### ProgressCircle

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `number \| null` | – | null spins an arc. |
| `min` | `number` | `0` |  |
| `max` | `number` | `100` |  |
| `size` | `"sm" \| "default" \| "lg" \| "xl"` | `"default"` |  |
| `variant` | `"default" \| "success" \| "warning" \| "destructive"` | `"default"` |  |
| `locale` | `Intl.LocalesArgument` | `"en-US"` |  |
| `children` | `ReactNode` | – | Shown in the middle of the ring. |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="progress-circle"` | The root. |
| `data-size` | The size: sm, default, lg or xl. |
| `data-variant` | The status variant. |
| `data-progressing` | Present while the value is below max. |
| `data-complete` | Present when the value reaches max. |
| `data-indeterminate` | Present when the value is null or not a finite number. |
| `--progress-circle-size` | The ring's width and height. |
| `--progress-stroke` | The ring's stroke width. |

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
