# Progress

> Progress bar for work under way (with an indeterminate state and tones) and Meter for a quantity against a limit that turns warning and danger past thresholds. Wraps Base UI Progress and Meter.

Source: https://docs.nasaqui.com/components/progress

## Install

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

Two thin bars that look alike and mean different things. `Progress` shows **a task moving toward done**: an
upload, an import, a setup. `Meter` shows **an amount against a limit**: seats used, storage, a monthly budget.
The fill starts at the inline start, so it grows from the right in Arabic.

## When to use

- `Progress`: something with a start and an end that takes long enough to see. Pass `value={null}` when the length is unknown.
- `Meter`: a quota or budget where crossing a threshold matters. It changes tone for you.

## When not to use

- A short wait with no length: use [`Spinner`](https://docs.nasaqui.com/components/spinner) or a [`LoadingState`](https://docs.nasaqui.com/components/states).
- A rating or score: use [`Rating`](https://docs.nasaqui.com/components/rating).
- A single status word: use [`Status`](https://docs.nasaqui.com/components/status) or [`Badge`](https://docs.nasaqui.com/components/badge).
- A written warning about a limit: pair a `Meter` with an [`Alert`](https://docs.nasaqui.com/components/alert).

## Import

```tsx
import { Meter, Progress } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { Progress } from "@fadymondy/nasaq/web";

export function Upload({ percent }: { percent: number }) {
  return <Progress value={percent} label="Uploading files" />;
}
```

## Anatomy

```
Progress / Meter          Base UI Root (role="progressbar" / role="meter")   data-slot="progress" | "meter"
├─ head                   label + value row (when label or showValue)        data-slot="progress-head"
└─ track                                                                     data-slot="progress-track" | "meter-track"
   └─ indicator           the fill                                           data-slot="progress-indicator" | "meter-indicator"
```

Both roots carry `data-tone` with the tone in use.

## API

### `Progress`

`ProgressProps` extends `ComponentProps<"div">` (without `children`).

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `number \| null` | required | Current value. `null` is indeterminate. |
| `tone?` | `"default" \| "info" \| "success" \| "warning" \| "danger"` | `"default"` | Fill colour. Set `success` when it completes. |
| `label?` | `ReactNode` | none | Visible name above the bar. Without one, pass `aria-label`. |
| `showValue?` | `boolean` | `true` when `label` is set | Shows the formatted value at the inline end. Hidden while indeterminate. |
| `valueText?` | `ReactNode` | formatted value | Replaces the visible value text ("45 of 50 seats"). |
| `format?` | `Intl.NumberFormatOptions` | percentage of the range | How the value is formatted. |
| `locale?` | `Intl.LocalesArgument` | runtime locale | Locale for numbers, e.g. `"ar-SA"`. |
| `size?` | `"sm" \| "md"` | `"md"` | Track height: 4px or 8px. |
| `min?` / `max?` | `number` | `0` / `100` | Range. |

### `Meter`

`MeterProps` shares the props above (`label`, `showValue`, `valueText`, `format`, `locale`, `size`, `min`, `max`) and has:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `number` | required | Current amount. Never `null`. |
| `warnAt?` | `number` | `0.8` | Fraction of the range at which the fill turns `warning`. |
| `dangerAt?` | `number` | `0.95` | Fraction of the range at which the fill turns `danger`. |
| `tone?` | `ProgressTone` | derived | Forces a tone and skips the thresholds. |

## Examples

### Unknown length, then determinate

```tsx
import { Progress } from "@fadymondy/nasaq/web";

export function Import({ percent }: { percent: number | null }) {
  return (
    <Progress
      value={percent}
      tone={percent === 100 ? "success" : "default"}
      label={percent === null ? "Preparing…" : "Importing"}
    />
  );
}
```

### Quota with visible text

```tsx
import { Meter } from "@fadymondy/nasaq/web";

export function Seats() {
  return <Meter value={42} max={50} label="Seats" valueText="42 of 50" />;
}
```

### Budget in Arabic, custom thresholds

```tsx
import { Meter } from "@fadymondy/nasaq/web";

export function BudgetAr() {
  return (
    <Meter
      value={7200}
      max={10000}
      warnAt={0.7}
      dangerAt={0.9}
      locale="ar-SA"
      format={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }}
      label="ميزانية الذكاء الاصطناعي"
    />
  );
}
```

## Accessibility

- `Progress` is `role="progressbar"` with `aria-valuenow`, `aria-valuemin`, `aria-valuemax` (no `aria-valuenow` when indeterminate). `Meter` is `role="meter"`.
- The `label` names the bar. If you omit `label`, pass `aria-label` or `aria-labelledby`.
- Colour is never the only carrier: the value text stays visible, and a threshold crossing should be backed by text (an [`Alert`](https://docs.nasaqui.com/components/alert) or `valueText`).
- Nothing is focusable and no keys apply.
- Indeterminate uses a pulse that stops under `prefers-reduced-motion`; width changes do not animate then either.

## RTL & i18n

- The fill is positioned with `inset-inline-start`, so it grows from the right in RTL. The head row (label, value) mirrors with the document.
- Numbers format with `locale` (pass `"ar-SA"` for Arabic-Indic digits). The component has no built-in strings; localise `label` and `valueText`.

## Styling & tokens

- Track `bg-nq-surface-soft`, fill `bg-primary` (default) or `bg-nq-info` / `-success` / `-warning` / `-danger`; `rounded-full`; 300ms width transition on `ease-nq`.
- Target `[data-slot=progress]`, `[data-slot=meter]`, `[data-tone=warning]`, `progress-track`, `progress-indicator`. Extend with `className`; set the width with a wrapper or `className` (the bar is `w-full`).

## Do / Don't

- **Do** use `Meter` for limits and `Progress` for tasks; the semantics differ for assistive tech.
- **Do** show the numbers ("42 of 50"), not just the bar.
- **Don't** use `danger` on a `Progress` unless the task failed; stop the bar and show an [`Alert`](https://docs.nasaqui.com/components/alert).
- **Don't** fake a percentage for unknown work: use `value={null}`.

## Related

- [Spinner](https://docs.nasaqui.com/components/spinner) · [States](https://docs.nasaqui.com/components/states) · [Alert](https://docs.nasaqui.com/components/alert) · [Status](https://docs.nasaqui.com/components/status)

## Lab

https://docs.nasaqui.com/?path=/docs/components-loading-states-progress--docs

## Code

### React

```tsx
import { Progress } from "@fadymondy/nasaq/web";

export function Upload({ percent }: { percent: number }) {
  return <Progress value={percent} label="Uploading files" />;
}
```

### shadcn

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

export function Upload({ percent }: { percent: number }) {
  return <Progress value={percent} label="Uploading files" />;
}
```

### Vue

```vue
<script setup lang="ts">
import { NqProgress } from "@fadymondy/nasaq/vue";
</script>

<template>
  <NqProgress :value="64" label="Uploading files" />
</template>
```

### Blade

```blade
<x-nq::progress :value="64" label="Uploading files" />

<div class="mt-6 flex flex-col gap-3" x-data="{ pct: 30 }">
    <x-nq::progress id="live-progress" :value="30" value-expr="pct" label="Live upload" />
    <x-nq::progress id="model-progress" :value="30" x-model="pct" label="Same value, x-model" />
    <div class="flex gap-2">
        <x-nq::button id="live-more" size="sm" x-on:click="pct = Math.min(100, pct + 35)">Add 35%</x-nq::button>
        <x-nq::button id="live-wait" size="sm" variant="outline" x-on:click="pct = null">Indeterminate</x-nq::button>
    </div>
</div>
```

### HTML + Alpine

```html
<div data-slot="progress" data-tone="default" role="progressbar"  aria-labelledby="nq-progress-5b10d3e7-label"     aria-valuemin="0" aria-valuemax="100"  aria-valuenow="64"     aria-valuetext="64%" data-progressing
    class="flex w-full flex-col gap-1.5">
            <div data-slot="progress-head" class="flex items-baseline justify-between gap-3 text-body-sm">
            <span id="nq-progress-5b10d3e7-label" class="text-label text-foreground">Uploading files</span>            <span aria-hidden="true" class="text-muted-foreground tabular-nums">64%</span>        </div>
        <div data-slot="progress-track" data-progressing class="relative block w-full overflow-hidden rounded-full bg-nq-surface-soft h-2">
        <div data-slot="progress-indicator" data-progressing style="inset-inline-start:0;width:64%"
            class="block h-full rounded-full transition-[width] duration-300 ease-nq motion-reduce:transition-none bg-primary"></div>
    </div>
</div>
<div class="mt-6 flex flex-col gap-3" x-data="{ pct: 30 }">
    <div data-slot="progress" data-tone="default" role="progressbar"  aria-labelledby="nq-progress-0d951ba4-label"     aria-valuemin="0" aria-valuemax="100"  aria-valuenow="30"     aria-valuetext="30%" data-progressing x-data="nqProgress(JSON.parse('{\u0022value\u0022:30,\u0022min\u0022:0,\u0022max\u0022:100,\u0022locale\u0022:\u0022en\u0022,\u0022fixedText\u0022:false}'))" x-modelable="value" x-effect="sync(pct)"
    id="live-progress" class="flex w-full flex-col gap-1.5">
            <div data-slot="progress-head" class="flex items-baseline justify-between gap-3 text-body-sm">
            <span id="nq-progress-0d951ba4-label" class="text-label text-foreground">Live upload</span>            <span aria-hidden="true" data-progress-value class="text-muted-foreground tabular-nums">30%</span>        </div>
        <div data-slot="progress-track" data-progressing class="relative block w-full overflow-hidden rounded-full bg-nq-surface-soft h-2">
        <div data-slot="progress-indicator" data-progressing style="inset-inline-start:0;width:30%"
            class="block h-full rounded-full transition-[width] duration-300 ease-nq motion-reduce:transition-none bg-primary"></div>
    </div>
</div>
    <div data-slot="progress" data-tone="default" role="progressbar"  aria-labelledby="nq-progress-73f23e22-label"     aria-valuemin="0" aria-valuemax="100"  aria-valuenow="30"     aria-valuetext="30%" data-progressing x-data="nqProgress(JSON.parse('{\u0022value\u0022:30,\u0022min\u0022:0,\u0022max\u0022:100,\u0022locale\u0022:\u0022en\u0022,\u0022fixedText\u0022:false}'))" x-modelable="value" x-effect="sync(value)"
    id="model-progress" x-model="pct" class="flex w-full flex-col gap-1.5">
            <div data-slot="progress-head" class="flex items-baseline justify-between gap-3 text-body-sm">
            <span id="nq-progress-73f23e22-label" class="text-label text-foreground">Same value, x-model</span>            <span aria-hidden="true" data-progress-value class="text-muted-foreground tabular-nums">30%</span>        </div>
        <div data-slot="progress-track" data-progressing class="relative block w-full overflow-hidden rounded-full bg-nq-surface-soft h-2">
        <div data-slot="progress-indicator" data-progressing style="inset-inline-start:0;width:30%"
            class="block h-full rounded-full transition-[width] duration-300 ease-nq motion-reduce:transition-none bg-primary"></div>
    </div>
</div>
    <div class="flex gap-2">
        <button data-slot="button"
     type="button"                         id="live-more" x-on:click="pct = Math.min(100, pct + 35)" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control-sm px-2.5">
        Add 35%</button>
        <button data-slot="button"
     type="button"                         id="live-wait" x-on:click="pct = null" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control-sm px-2.5">
        Indeterminate</button>
    </div>
</div>
```
