# Progress (/ui/components/react/progress)



<Playground component="progress" />

## Usage [#usage]

```tsx
import { Progress, ProgressLabel, ProgressValue } from '@appica/ui-react/progress'
```

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

`Progress` tracks a task **moving toward completion** — an upload, an import, an install — unlike [`Meter`](/ui/components/react/meter), which shows a static measurement. Set `value` from `0` to `100` (or pass `min`/`max` for a custom range). When you can't compute a percentage — an unknown-duration wait — reach for a [`Loader`](/ui/components/react/loader) or [`Spinner`](/ui/components/react/spinner) instead.

It comes in two layouts via `variant`: `bar` (a horizontal track, the default) with `ProgressLabel` and `ProgressValue` arranged above it, and `circular` (an SVG ring) with the value centered inside. Recolor the fill with `indicatorColor`, and tune its weight with `thickness` (plus `size` for the ring).

## Examples [#examples]

### Default [#default]

The `bar` layout: a label on the left, the percentage on the right, and the track spanning beneath. `ProgressValue` formats `value` as a percentage by default.

```tsx
import { Progress, ProgressLabel, ProgressValue } from '@appica/ui-react/progress'

export default function ProgressDefault() {
  return (
    <Progress value={60} className="w-72 max-w-full">
      <ProgressLabel>Uploading</ProgressLabel>
      <ProgressValue />
    </Progress>
  )
}
```

### Circular [#circular]

`variant="circular"` draws an SVG ring with the value in its center. Set its diameter with `size`; an optional `ProgressLabel` drops below the ring.

```tsx
import { Progress, ProgressLabel, ProgressValue } from '@appica/ui-react/progress'

export default function ProgressCircular() {
  return (
    <div className="flex items-center gap-10">
      <Progress variant="circular" value={72} size={64}>
        <ProgressValue />
      </Progress>

      <Progress variant="circular" value={40} size={64}>
        <ProgressValue />
        <ProgressLabel>Synced</ProgressLabel>
      </Progress>
    </div>
  )
}
```

### Custom color & thickness [#custom-color--thickness]

`indicatorColor` accepts any CSS color (a theme token like `var(--success-emphasis)` keeps it on-brand), and `thickness` sets the track weight in pixels — for both the bar and the ring.

```tsx
import { Progress, ProgressLabel, ProgressValue } from '@appica/ui-react/progress'

export default function ProgressCustom() {
  return (
    <div className="flex w-72 max-w-full flex-col gap-7">
      <Progress value={88} thickness={10} indicatorColor="var(--success-emphasis)">
        <ProgressLabel>Backup</ProgressLabel>
        <ProgressValue />
      </Progress>

      <Progress value={24} thickness={2} indicatorColor="var(--warning-emphasis)">
        <ProgressLabel>Storage</ProgressLabel>
        <ProgressValue />
      </Progress>

      <div className="flex justify-center">
        <Progress variant="circular" value={62} size={72} thickness={8} indicatorColor="var(--secondary-emphasis)">
          <ProgressValue />
        </Progress>
      </div>
    </div>
  )
}
```

### File upload (controlled) [#file-upload-controlled]

A real-world card driven by your own state: advance `value` as bytes arrive, swap the label to "Uploaded", and reveal an action once it completes. The fill animates smoothly between values.

```tsx
'use client'

import * as React from 'react'
import { Progress, ProgressLabel, ProgressValue } from '@appica/ui-react/progress'
import { Button } from '@appica/ui-react/button'
import { Photo } from '@appica/icons-react'
import { cn } from '@/lib/utils'

function AnimatedCheck() {
  const [drawn, setDrawn] = React.useState(false)

  React.useEffect(() => {
    const id = requestAnimationFrame(() => setDrawn(true))
    return () => cancelAnimationFrame(id)
  }, [])

  return (
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="size-5">
      <path
        d="M5 12.5l4.5 4.5L19 7"
        stroke="currentColor"
        strokeWidth={2}
        strokeLinecap="round"
        strokeLinejoin="round"
        pathLength={1}
        className="transition-[stroke-dashoffset] duration-500 ease-out [stroke-dasharray:1] motion-reduce:transition-none"
        style={{ strokeDashoffset: drawn ? 0 : 1 }}
      />
    </svg>
  )
}

export default function ProgressUpload() {
  const [progress, setProgress] = React.useState(0)
  const [running, setRunning] = React.useState(true)

  React.useEffect(() => {
    if (!running) return
    const id = setInterval(() => {
      setProgress((current) => {
        if (current >= 100) {
          clearInterval(id)
          setRunning(false)
          return 100
        }
        return Math.min(100, current + 7)
      })
    }, 280)
    return () => clearInterval(id)
  }, [running])

  const done = progress >= 100

  const restart = () => {
    setProgress(0)
    setRunning(true)
  }

  return (
    <div className="border-border bg-background w-80 max-w-full rounded-xl border p-4">
      <div className="flex items-center gap-3">
        <span
          className={cn(
            'flex size-12 shrink-0 items-center justify-center rounded-lg transition-colors',
            done ? 'bg-success-subtle text-success-emphasis' : 'bg-background-muted text-foreground-muted',
          )}
        >
          {done ? <AnimatedCheck /> : <Photo className="size-5" />}
        </span>
        <div className="min-w-0 flex-1">
          <p className="text-foreground-intense truncate text-sm font-medium">cover-photo.jpg</p>
          <Progress value={progress} className="mt-1.5 gap-y-1">
            <ProgressValue className="text-foreground-muted text-xs" />
            <ProgressLabel className="text-foreground-muted text-xs font-normal">
              {done ? 'Uploaded' : 'Uploading…'}
            </ProgressLabel>
          </Progress>
        </div>
      </div>
      {done && (
        <Button variant="soft" size="sm" onClick={restart} className="mt-3 w-full">
          Upload again
        </Button>
      )}
    </div>
  )
}
```

## RTL [#rtl]

Every Appica UI component supports right-to-left layouts out of the box. Set the `dir` attribute on a container (commonly your `<html>` element) so CSS logical properties resolve correctly, and wrap your tree in `DirectionProvider` so direction-aware behavior follows the same direction.

```tsx
import { DirectionProvider } from '@appica/ui-react/providers/direction-provider'

export default function RootLayout({ children }) {
  return (
    <html lang="ar" dir="rtl">
      <body>
        <DirectionProvider dir="rtl">{children}</DirectionProvider>
      </body>
    </html>
  )
}
```

Under RTL the label and value swap sides and the bar fills from the right. For setup details and caveats, see the [RTL guide](/ui/docs/react/rtl).

<RtlPreview component="progress" />

## API reference [#api-reference]

`Progress` wraps [Base UI's Progress](https://base-ui.com/react/components/progress). Each part forwards `ref` and its remaining native attributes to the matching Base UI primitive — the tables below list the Appica additions and the most common props. The root exposes `data-complete`, `data-progressing`, and `data-variant` for styling.

### Progress [#progress]

The root. Owns the `value`, range, layout, and indicator styling, and lays out the label, value, and track. Renders a `<div role="progressbar">`.

| Prop               | <ColMinWidth width="210">Type</ColMinWidth>                   | Default       | <ColMinWidth width="220">Description</ColMinWidth>                          |
| ------------------ | ------------------------------------------------------------- | ------------- | --------------------------------------------------------------------------- |
| `value`            | <Code>number \| null</Code>                                   | —             | Current value, from `min` to `max`. Pass `null` for an indeterminate state. |
| `variant`          | <Code>'bar' \| 'circular'</Code>                              | `'bar'`       | Horizontal track or SVG ring.                                               |
| `min`              | `number`                                                      | `0`           | Lower bound of the range.                                                   |
| `max`              | `number`                                                      | `100`         | Upper bound of the range.                                                   |
| `indicatorColor`   | `string`                                                      | primary token | Any CSS color for the fill (e.g. `var(--success-emphasis)`).                |
| `thickness`        | `number`                                                      | `6` / `4`     | Track weight in pixels. Defaults to `6` for `bar`, `4` for `circular`.      |
| `size`             | `number`                                                      | `56`          | Diameter of the ring in pixels (`circular` only).                           |
| `format`           | `Intl.NumberFormatOptions`                                    | —             | Formatting passed to `Intl.NumberFormat` for the displayed value.           |
| `locale`           | `Intl.LocalesArgument`                                        | —             | Locale used by `Intl.NumberFormat`.                                         |
| `getAriaValueText` | <Code>(formatted, value) => string</Code>                     | —             | Build the human-readable `aria-valuetext`.                                  |
| `render`           | <Code>ReactElement \| ((props, state) => ReactElement)</Code> | —             | Replace the rendered element, or compose it with another component.         |
| `className`        | <Code>string \| ((state) => string)</Code>                    | —             | Extra classes, merged via `tailwind-merge`. Set the bar width here.         |
| `style`            | <Code>CSSProperties \| ((state) => CSSProperties)</Code>      | —             | Inline styles, optionally derived from state.                               |

### ProgressLabel [#progresslabel]

Names the task and is wired to the root via `aria-labelledby`. Renders a `<span>`.

| Prop        | <ColMinWidth width="210">Type</ColMinWidth>                   | Default | Description                                   |
| ----------- | ------------------------------------------------------------- | ------- | --------------------------------------------- |
| `render`    | <Code>ReactElement \| ((props, state) => ReactElement)</Code> | —       | Replace or compose the rendered element.      |
| `className` | <Code>string \| ((state) => string)</Code>                    | —       | Extra classes, merged via `tailwind-merge`.   |
| `style`     | <Code>CSSProperties \| ((state) => CSSProperties)</Code>      | —       | Inline styles, optionally derived from state. |

### ProgressValue [#progressvalue]

Prints the formatted value. Renders a `<span>`.

| Prop        | <ColMinWidth width="210">Type</ColMinWidth>                            | Default | <ColMinWidth width="220">Description</ColMinWidth>                     |
| ----------- | ---------------------------------------------------------------------- | ------- | ---------------------------------------------------------------------- |
| `children`  | <Code>((formatted: string, value: number) => ReactNode) \| null</Code> | —       | Render function to customize the displayed text. Omit for the default. |
| `render`    | <Code>ReactElement \| ((props, state) => ReactElement)</Code>          | —       | Replace or compose the rendered element.                               |
| `className` | <Code>string \| ((state) => string)</Code>                             | —       | Extra classes, merged via `tailwind-merge`.                            |
| `style`     | <Code>CSSProperties \| ((state) => CSSProperties)</Code>               | —       | Inline styles, optionally derived from state.                          |

## Accessibility [#accessibility]

* `Progress` renders `role="progressbar"` with `aria-valuenow`, `aria-valuemin`, and `aria-valuemax` reflecting `value`, `min`, and `max`.
* Give every progress bar an accessible name — a `ProgressLabel` (wired via `aria-labelledby`) or an `aria-label` on the root when no visible label is shown.
* The circular SVG is marked `aria-hidden`; the accessible value comes from the root's ARIA attributes, so the ring is purely visual.
* The fill animation is replaced by a static state when the user prefers reduced motion.
