Progress
A bar or ring that shows how far a task has come, eases between updates and slides while the total is unknown.
pnpm dlx shadcn@latest add https://hextaui.com/r/progress.jsonAdds the component, the HextaUI theme tokens and any HextaUI components it depends on.
Add the theme tokens to your global CSS, if you haven’t yet.
Install the dependencies.
pnpm add @base-ui/react class-variance-authority cnCopy and paste the following code into your project.
components/ui/progress.tsx Update the import paths to match your project setup.
<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.
Sizes
xs, sm, default and lg change the bar's thickness. xs is the hairline Attachment draws along its bottom edge.
Status
variant colors only the fill or ring, so the track, label and value stay neutral.
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.
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.
Indeterminate circle
An arc spins around the ring until a value arrives.
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.
Animated value
Render <NumberFlow /> inside <ProgressValue /> so only the digits that change spin, in step with the fill.
Long labels
Long names wrap onto their own lines and the value stays on the end. Rings work as compact status beside each row.
- quarterly-report-final-v3-approved-by-legal-and-finance.pdf
- IMG_20260914_183022_HDR_edited_export.jpg
- brand-assets.zip
Without a visible label
Name the bar with aria-label when the context already says what is loading.
Right to left
The fill and the indeterminate slide start from the right. Pass locale to format the value in the reader's digits.
- The root is a
progressbarwitharia-valuenow,aria-valuemin,aria-valuemaxand a formattedaria-valuetext. While indeterminate it has no current value. <ProgressLabel />names the bar. Without one, passaria-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-USunless you passlocale, so the server and browser render the same text.
Built on the Base UI progress. Every part accepts the props of the primitive it wraps.
| Prop | Type | Default |
|---|---|---|
valuenull makes the bar indeterminate. | number | null | – |
min | number | 0 |
max | number | 100 |
size | "xs" | "sm" | "default" | "lg" | "default" |
variant | "default" | "success" | "warning" | "destructive" | "default" |
formatFormats the value. Without it, the value shows as a percentage. | Intl.NumberFormatOptions | – |
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. |
Names the progressbar. Renders a <span> and takes the same state attributes as the root.
| Prop | Type | Default |
|---|---|---|
render | ReactElement | (props, state) => ReactElement | <span> |
| Attribute | Description |
|---|---|
data-slot="progress-label" | The label. |
| Prop | Type | Default |
|---|---|---|
childrenCustom text. Without it, the formatted value shows, or nothing while indeterminate. | (formattedValue: string | null, value: number | null) => ReactNode | – |
render | ReactElement | (props, state) => ReactElement | <span> |
| Attribute | Description |
|---|---|
data-slot="progress-value" | The value. |
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. |
The fill. Its width is set inline from the value and eases between updates.
| Attribute | Description |
|---|---|
data-slot="progress-indicator" | The fill. |
| Prop | Type | Default |
|---|---|---|
valuenull spins an arc. | number | null | – |
min | number | 0 |
max | number | 100 |
size | "sm" | "default" | "lg" | "xl" | "default" |
variant | "default" | "success" | "warning" | "destructive" | "default" |
locale | Intl.LocalesArgument | "en-US" |
childrenShown in the middle of the ring. | ReactNode | – |
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. |