# Progress (/docs/progress)





`Progress` is a linear bar built on [Ark UI](https://ark-ui.com)'s Progress state machine, so it ships `role="progressbar"`, the `aria-value*` wiring, and indeterminate support for free. The fill axis (`filled` / `gradient`) and the palette mirror [`Badge`](/docs/badge), so a bar and a chip share the same accent language.

```tsx
import { Progress } from '@42-central/ui-react/progress';

<Progress value={60} />
<Progress variant="gradient" value={70} />
<Progress value={null} /> {/* indeterminate */}
```

## Playground [#playground]

<story.Playground />

### Full prop browser [#full-prop-browser]

<story.WithControl />

## Variants [#variants]

Two fill treatments. `filled` (the default) routes through the palette; `gradient` paints its own stops.

<Preview align="block" code="&#x22;<Progress variant=\&#x22;filled\&#x22; value={60} />\n<Progress variant=\&#x22;gradient\&#x22; value={60} />&#x22;">
  <div className="flex w-full flex-col gap-4">
    <Progress variant="filled" value="60" />

    <Progress variant="gradient" value="60" />
  </div>
</Preview>

## Colors [#colors]

Any palette color pairs with the `filled` variant via the kit's `data-color` system. Use it for status semantics: `green` for healthy, `orange` for warning, `red` for critical.

<Preview align="block" code="&#x22;<Progress color=\&#x22;green\&#x22; value={70} />\n<Progress color=\&#x22;orange\&#x22; value={45} />\n<Progress color=\&#x22;red\&#x22; value={20} />&#x22;">
  <div className="flex w-full flex-col gap-3">
    {COLORS.map((c) => (
          <Progress key={c} color={c} value={66} />
        ))}
  </div>
</Preview>

## Gradient [#gradient]

`variant="gradient"` defaults to a purple→pink sweep. Pass a `gradient` config (`from` / `to` / `via` / `dir`) to customize the stops and direction — same API as [`Badge`](/docs/badge).

The gradient spans the **full track**, not just the filled portion — the bar reveals a left slice of it, so the complete `from`→`to` only appears at 100%.

<Preview align="block" code="&#x22;<Progress variant=\&#x22;gradient\&#x22; value={70} />\n<Progress variant=\&#x22;gradient\&#x22; value={70} gradient={{ from: 'var(--color-cyan-400)', to: 'var(--color-blue-600)' }} />&#x22;">
  <div className="flex w-full flex-col gap-4">
    <Progress variant="gradient" value="70" />

    <Progress variant="gradient" value="70" gradient="{ from: 'var(--color-cyan-400)', to: 'var(--color-blue-600)' }" />
  </div>
</Preview>

## Sizes [#sizes]

Five track heights, matching the kit's `xs`–`xl` scale ([`Slider`](/docs/slider) uses the same).

<Preview align="block" code="&#x22;<Progress size=\&#x22;xs\&#x22; value={60} />\n<Progress size=\&#x22;sm\&#x22; value={60} />\n<Progress size=\&#x22;md\&#x22; value={60} />\n<Progress size=\&#x22;lg\&#x22; value={60} />\n<Progress size=\&#x22;xl\&#x22; value={60} />&#x22;">
  <div className="flex w-full flex-col gap-4">
    <Progress size="xs" value="60" />

    <Progress size="sm" value="60" />

    <Progress size="md" value="60" />

    <Progress size="lg" value="60" />

    <Progress size="xl" value="60" />
  </div>
</Preview>

## Indeterminate [#indeterminate]

When you can't measure progress, pass `value={null}` (or omit both `value` and `defaultValue` and call it indeterminate). The bar runs an animated sweep — and under the OS "reduce motion" setting it rests as a static partial bar instead of looping.

<Preview align="block" code="`<Progress value={null} />`">
  <div className="flex w-full flex-col gap-4">
    <Progress value="null" />

    <Progress value="null" color="green" />

    <Progress value="null" variant="gradient" />
  </div>
</Preview>

## API [#api]

<TypeTable
  type="{
  variant: {
    type: &#x22;'filled' | 'gradient'&#x22;,
    default: &#x22;'filled'&#x22;,
    description: 'Fill treatment. `filled` use the palette; `gradient` paints its own stops.',
  },
  value: {
    type: 'number | null',
    description: 'Controlled value in min..max. Pass `null` for the indeterminate state.',
  },
  defaultValue: {
    type: 'number | null',
    description: 'Uncontrolled initial value.',
  },
  onChange: {
    type: '(value: number | null) => void',
    description: 'Fires on every value change.',
  },
  min: {
    type: 'number',
    default: '0',
    description: 'Lower bound of the value space.',
  },
  max: {
    type: 'number',
    default: '100',
    description: 'Upper bound of the value space.',
  },
  color: {
    type: &#x22;'brand' | 'neutral' | 'red' | 'orange' | 'yellow' | 'green' | 'teal' | 'cyan' | 'blue' | 'purple' | 'pink' | (string & {})&#x22;,
    default: &#x22;'brand'&#x22;,
    description: 'Accent palette. Ignored when variant is `gradient`.',
  },
  gradient: {
    type: '{ from?: string; to?: string; via?: string; dir?: GradientDir }',
    description: 'Gradient stops + direction. Only applied when variant is `gradient`.',
  },
  size: {
    type: &#x22;'xs' | 'sm' | 'md' | 'lg' | 'xl'&#x22;,
    default: &#x22;'md'&#x22;,
    description: 'Track height.',
  },
  testId: {
    type: 'string',
    description: 'Renders as `data-testid` on the Track — the actual `role=&#x22;progressbar&#x22;` node — not the outer root wrapper. See [Testing](/docs/testing).',
  },
}"
/>

All other props are forwarded to the underlying Ark `Progress.Root`.

## Accessibility [#accessibility]

Ark stamps `role="progressbar"` plus `aria-valuenow` / `aria-valuemin` / `aria-valuemax` on the bar. In the indeterminate state `aria-valuenow` is dropped, so assistive tech announces an unmeasured "busy" bar. Provide an `aria-label` (or wire `aria-labelledby` to a visible caption) so the bar names what it's tracking:

```tsx
<Progress value={uploaded} aria-label="Upload progress" />
```
