Progress

A completion bar with an optional label and live value.

Show completion with a plain bar, a labeled live value, or a value driven by another control.

tsx
"use client"import * as React from "react"import { Button } from "@/components/ui/button"import { Progress } from "@/components/ui/progress"const DURATION_MS = 2800export function Example() {  const [run, setRun] = React.useState(0)  const progress = useProgressRun(run)  return (    <div className="relative w-full">      <Button        type="button"        variant="ghost"        size="icon-sm"        className="absolute top-0 right-0 z-20"        aria-label="Reload"        onClick={() => setRun((n) => n + 1)}      >        <ReloadIcon />      </Button>      <Progress        value={progress}        className="w-full [&_[data-slot=progress-indicator]]:duration-0"      />    </div>  )}function useProgressRun(run: number) {  const [progress, setProgress] = React.useState(0)  React.useEffect(() => {    setProgress(0)    const start = performance.now()    let frame = 0    function tick(now: number) {      const t = Math.min(1, (now - start) / DURATION_MS)      setProgress(Math.round(t * 100))      if (t < 1) frame = requestAnimationFrame(tick)    }    frame = requestAnimationFrame(tick)    return () => cancelAnimationFrame(frame)  }, [run])  return progress}function ReloadIcon() {  return (    <svg      xmlns="http://www.w3.org/2000/svg"      viewBox="0 0 24 24"      fill="none"      stroke="currentColor"      strokeWidth="2"      strokeLinecap="round"      strokeLinejoin="round"      className="size-3.5"      aria-hidden    >      <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />      <path d="M21 3v5h-5" />      <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />      <path d="M8 16H3v5" />    </svg>  )}

Installation

npx @arctis-sh/@arctis-sh/ui@latest add progress

Usage

import { Progress } from "@/components/ui/progress"
<Progress value={33} />

Composition

Progress
├── ProgressLabel
├── ProgressValue
└── ProgressTrack
    └── ProgressIndicator

ProgressTrack and ProgressIndicator are included automatically. Add ProgressLabel and ProgressValue when you want a header row.

Basic

A plain bar from 0 to 100. Reload the example to replay the fill animation.

tsx
"use client"import * as React from "react"import { Button } from "@/components/ui/button"import { Progress } from "@/components/ui/progress"const DURATION_MS = 2800export function Example() {  const [run, setRun] = React.useState(0)  const progress = useProgressRun(run)  return (    <div className="relative w-full">      <Button        type="button"        variant="ghost"        size="icon-sm"        className="absolute top-0 right-0 z-20"        aria-label="Reload"        onClick={() => setRun((n) => n + 1)}      >        <ReloadIcon />      </Button>      <Progress        value={progress}        className="w-full [&_[data-slot=progress-indicator]]:duration-0"      />    </div>  )}function useProgressRun(run: number) {  const [progress, setProgress] = React.useState(0)  React.useEffect(() => {    setProgress(0)    const start = performance.now()    let frame = 0    function tick(now: number) {      const t = Math.min(1, (now - start) / DURATION_MS)      setProgress(Math.round(t * 100))      if (t < 1) frame = requestAnimationFrame(tick)    }    frame = requestAnimationFrame(tick)    return () => cancelAnimationFrame(frame)  }, [run])  return progress}function ReloadIcon() {  return (    <svg      xmlns="http://www.w3.org/2000/svg"      viewBox="0 0 24 24"      fill="none"      stroke="currentColor"      strokeWidth="2"      strokeLinecap="round"      strokeLinejoin="round"      className="size-3.5"      aria-hidden    >      <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />      <path d="M21 3v5h-5" />      <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />      <path d="M8 16H3v5" />    </svg>  )}

Label

Add ProgressLabel and ProgressValue for a title and live percentage.

Upload progress0%
tsx
"use client"import * as React from "react"import { Button } from "@/components/ui/button"import {  Progress,  ProgressLabel,  ProgressValue,} from "@/components/ui/progress"const DURATION_MS = 2800export function Example() {  const [run, setRun] = React.useState(0)  const progress = useProgressRun(run)  return (    <div className="relative w-full">      <Button        type="button"        variant="ghost"        size="icon-sm"        className="absolute top-0 right-0 z-20"        aria-label="Reload"        onClick={() => setRun((n) => n + 1)}      >        <ReloadIcon />      </Button>      <Progress        value={progress}        className="w-full [&_[data-slot=progress-indicator]]:duration-0"      >        <ProgressLabel>Upload progress</ProgressLabel>        <ProgressValue />      </Progress>    </div>  )}function useProgressRun(run: number) {  const [progress, setProgress] = React.useState(0)  React.useEffect(() => {    setProgress(0)    const start = performance.now()    let frame = 0    function tick(now: number) {      const t = Math.min(1, (now - start) / DURATION_MS)      setProgress(Math.round(t * 100))      if (t < 1) frame = requestAnimationFrame(tick)    }    frame = requestAnimationFrame(tick)    return () => cancelAnimationFrame(frame)  }, [run])  return progress}function ReloadIcon() {  return (    <svg      xmlns="http://www.w3.org/2000/svg"      viewBox="0 0 24 24"      fill="none"      stroke="currentColor"      strokeWidth="2"      strokeLinecap="round"      strokeLinejoin="round"      className="size-3.5"      aria-hidden    >      <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />      <path d="M21 3v5h-5" />      <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />      <path d="M8 16H3v5" />    </svg>  )}

Controlled

Drive the bar from another control. This example uses a Slider.

Export40%
tsx
"use client"import * as React from "react"import {  Progress,  ProgressLabel,  ProgressValue,} from "@/components/ui/progress"import { Slider } from "@/components/ui/slider"export function Example() {  const [value, setValue] = React.useState([40])  return (    <div className="grid w-full gap-4">      <Progress        value={value[0] ?? 0}        className="w-full [&_[data-slot=progress-indicator]]:duration-0"      >        <ProgressLabel>Export</ProgressLabel>        <ProgressValue />      </Progress>      <Slider        value={value}        onValueChange={(next) => {          const nextValue = readSliderValue(next)          setValue((prev) => (prev[0] === nextValue ? prev : [nextValue]))        }}        min={0}        max={100}        step={1}      />    </div>  )}function readSliderValue(next: number | readonly number[]) {  if (typeof next === "number") return next  if (Array.isArray(next)) return next[0] ?? 0  return 0}

API Reference

Progress

value is clamped between 0 and max.

PropTypeDefault
valuenumber0
maxnumber100
classNamestring
<Progress value={60} max={100} />

ProgressLabel

Sets aria-labelledby on the progressbar when present.

PropTypeDefault
classNamestring
<Progress>  <ProgressLabel>Uploading</ProgressLabel>  <ProgressValue /></Progress>

ProgressValue

Defaults to a rounded percentage. Pass children or a render function to customize.

PropTypeDefault
childrenReactNode | ((value: number, max: number) => ReactNode)
classNamestring
<ProgressValue />

ProgressTrack

Optional. Rendered automatically when omitted.

PropTypeDefault
classNamestring
<ProgressTrack>  <ProgressIndicator /></ProgressTrack>

ProgressIndicator

Optional. Rendered automatically inside the track when omitted. Width follows value / max. Defaults to a duration-300 width transition — override with className when you want instant updates.

PropTypeDefault
classNamestring
<ProgressIndicator />