HextaUI

Progress

Une barre ou un anneau qui montre l’avancement d’une tâche, s’adoucit entre les mises à jour et glisse tant que le total est inconnu.

Preparing…
x
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  Progress,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"

export function ProgressDemo() {
  const [value, setValue] = React.useState<number | null>(null)
  const [run, setRun] = React.useState(0)

  React.useEffect(() => {
    let current = 0
    let tick: ReturnType<typeof setInterval> | undefined
    const start = setTimeout(() => {
      setValue(0)
      tick = setInterval(() => {
        current = Math.min(100, current + Math.round(Math.random() * 12 + 3))
        setValue(current)
        if (current === 100) {
          clearInterval(tick)
        }
      }, 400)
    }, 1200)
    return () => {
      clearTimeout(start)
      clearInterval(tick)
    }
  }, [run])

  const label =
    value === null ? "Preparing…" : value === 100 ? "Uploaded" : "Uploading"

  return (
    <div className="flex w-full max-w-sm flex-col items-center gap-6">
      <Progress value={value}>
        <ProgressLabel>{label}</ProgressLabel>
        <ProgressValue />
      </Progress>
      <Button
        variant="outline"
        size="sm"
        onClick={() => {
          setValue(null)
          setRun(run + 1)
        }}
      >
        Restart
      </Button>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/progress.json

Ajoute le composant, les tokens de thème HextaUI et les composants HextaUI dont il dépend.

import {
  Progress,
  ProgressCircle,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"
<Progress value={40}>
  <ProgressLabel>Uploading</ProgressLabel>
  <ProgressValue />
</Progress>

<ProgressCircle value={40} aria-label="Uploading" />

<Progress /> trace sa propre piste et son indicateur après ses enfants, si bien qu'un label et une valeur tiennent sur une ligne au-dessus de la barre. Chaque mise à jour adoucit le remplissage depuis sa position actuelle : des mises à jour rapides forment un seul mouvement fluide plutôt que des paliers.

Progress
├── ProgressLabel
└── ProgressValue

ProgressCircle
└── ProgressValue

Tailles

xs, sm, default et lg changent l'épaisseur de la barre. xs est le filet qu'Attachment trace le long de son bord inférieur.

import {
  Progress,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"

export function ProgressSizes() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-6">
      <Progress value={15} size="xs">
        <ProgressLabel>Extra small</ProgressLabel>
        <ProgressValue />
      </Progress>
      <Progress value={30} size="sm">
        <ProgressLabel>Small</ProgressLabel>
        <ProgressValue />
      </Progress>
      <Progress value={55}>
        <ProgressLabel>Default</ProgressLabel>
        <ProgressValue />
      </Progress>
      <Progress value={80} size="lg">
        <ProgressLabel>Large</ProgressLabel>
        <ProgressValue />
      </Progress>
    </div>
  )
}

Statut

variant ne colore que le remplissage ou l'anneau : la piste, le label et la valeur restent neutres.

import {
  Progress,
  ProgressCircle,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"

export function ProgressVariants() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-6">
      <Progress value={100} variant="success">
        <ProgressLabel>Backup complete</ProgressLabel>
        <ProgressValue />
      </Progress>
      <Progress value={86} variant="warning">
        <ProgressLabel>Storage almost full</ProgressLabel>
        <ProgressValue />
      </Progress>
      <Progress value={47} variant="destructive">
        <ProgressLabel>Upload failed</ProgressLabel>
        <ProgressValue />
      </Progress>
      <div className="flex items-center gap-4">
        <ProgressCircle value={100} variant="success" aria-label="Synced" />
        <ProgressCircle value={86} variant="warning" aria-label="Almost full" />
        <ProgressCircle value={47} variant="destructive" aria-label="Failed" />
      </div>
    </div>
  )
}

Indéterminé

Passez value={null} tant que le total est inconnu. Un segment glisse le long de la piste, et dès qu'un nombre arrive, le remplissage grandit depuis le début.

import { Progress, ProgressLabel } from "@/components/ui/progress"

export function ProgressIndeterminate() {
  return (
    <div className="w-full max-w-sm">
      <Progress value={null}>
        <ProgressLabel>Connecting to server…</ProgressLabel>
      </Progress>
    </div>
  )
}

Cercle

<ProgressCircle /> trace la même valeur sous forme d'anneau, en partant du haut. Les enfants se placent au centre, ce qui convient à <ProgressValue /> en lg et xl.

import { ProgressCircle, ProgressValue } from "@/components/ui/progress"

export function ProgressCircleDemo() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-8">
      <ProgressCircle value={25} size="sm" aria-label="Small" />
      <ProgressCircle value={50} aria-label="Default" />
      <ProgressCircle value={75} size="lg" aria-label="Large">
        <ProgressValue />
      </ProgressCircle>
      <ProgressCircle value={100} size="xl" aria-label="Extra large">
        <ProgressValue />
      </ProgressCircle>
    </div>
  )
}

Cercle indéterminé

Un arc tourne autour de l'anneau jusqu'à l'arrivée d'une valeur.

import { ProgressCircle } from "@/components/ui/progress"

export function ProgressCircleIndeterminate() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-8">
      <ProgressCircle value={null} size="sm" aria-label="Syncing" />
      <ProgressCircle value={null} aria-label="Syncing" />
      <ProgressCircle value={null} size="lg" aria-label="Syncing" />
      <ProgressCircle value={null} size="xl" aria-label="Syncing" />
    </div>
  )
}

Plage et format personnalisés

Définissez min et max pour n'importe quelle plage, format pour le nombre, et une fonction enfant sur <ProgressValue /> pour le texte. Donnez aux lecteurs d'écran les mêmes mots avec getAriaValueText.

"use client"

import {
  Progress,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"

export function ProgressFormat() {
  return (
    <div className="w-full max-w-sm">
      <Progress
        value={37.5}
        max={50}
        format={{ maximumFractionDigits: 1 }}
        getAriaValueText={(formatted) => `${formatted} of 50 GB used`}
      >
        <ProgressLabel>Storage</ProgressLabel>
        <ProgressValue>{(formatted) => `${formatted} of 50 GB`}</ProgressValue>
      </Progress>
    </div>
  )
}

Valeur animée

Rendez <NumberFlow /> dans <ProgressValue /> pour que seuls les chiffres qui changent tournent, en phase avec le remplissage.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { NumberFlow } from "@/components/ui/number-flow"
import {
  Progress,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"

export function ProgressNumberFlow() {
  const [value, setValue] = React.useState(42)

  return (
    <div className="flex w-full max-w-sm flex-col items-center gap-6">
      <Progress value={value}>
        <ProgressLabel>Course completed</ProgressLabel>
        <ProgressValue>
          {(_, current) => <NumberFlow value={current ?? 0} suffix="%" />}
        </ProgressValue>
      </Progress>
      <div className="flex gap-2">
        <Button
          variant="outline"
          size="sm"
          onClick={() => setValue(Math.max(0, value - 13))}
        >
          −13
        </Button>
        <Button
          variant="outline"
          size="sm"
          onClick={() => setValue(Math.min(100, value + 13))}
        >
          +13
        </Button>
      </div>
    </div>
  )
}

Libellés longs

Les noms longs passent sur leurs propres lignes et la valeur reste à la fin. Les anneaux servent de statut compact à côté de chaque ligne.

import {
  Progress,
  ProgressCircle,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"

const files = [
  {
    name: "quarterly-report-final-v3-approved-by-legal-and-finance.pdf",
    value: 64,
  },
  { name: "IMG_20260914_183022_HDR_edited_export.jpg", value: 100 },
  { name: "brand-assets.zip", value: 12 },
]

export function ProgressFiles() {
  return (
    <ul className="flex w-full max-w-sm flex-col gap-5">
      {files.map((file) => (
        <li key={file.name} className="flex items-start gap-3">
          <ProgressCircle value={file.value} aria-label={file.name} />
          <div className="min-w-0 flex-1">
            <Progress value={file.value} size="sm">
              <ProgressLabel>{file.name}</ProgressLabel>
              <ProgressValue />
            </Progress>
          </div>
        </li>
      ))}
    </ul>
  )
}

Sans label visible

Nommez la barre avec aria-label quand le contexte indique déjà ce qui se charge.

import { Progress } from "@/components/ui/progress"

export function ProgressUnlabeled() {
  return (
    <div className="w-full max-w-sm">
      <Progress value={45} aria-label="Profile setup" />
    </div>
  )
}

De droite à gauche

Le remplissage et le glissement indéterminé partent de la droite. Passez locale pour formater la valeur avec les chiffres du lecteur.

import {
  Progress,
  ProgressCircle,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress"

export function ProgressRtl() {
  return (
    <div dir="rtl" className="flex w-full max-w-sm flex-col gap-6">
      <Progress value={65} locale="ar-EG">
        <ProgressLabel>جارٍ التحميل</ProgressLabel>
        <ProgressValue />
      </Progress>
      <Progress value={null}>
        <ProgressLabel>جارٍ الاتصال…</ProgressLabel>
      </Progress>
      <ProgressCircle value={65} size="xl" locale="ar-EG" aria-label="التقدم">
        <ProgressValue />
      </ProgressCircle>
    </div>
  )
}
  • La racine est une progressbar avec aria-valuenow, aria-valuemin, aria-valuemax et un aria-valuetext formaté. Tant qu'elle est indéterminée, elle n'a pas de valeur courante.
  • <ProgressLabel /> nomme la barre. Sans lui, passez aria-label.
  • <ProgressValue /> est masqué aux lecteurs d'écran, puisque la progressbar annonce déjà la valeur.
  • Avec la réduction des animations, le remplissage saute à chaque nouvelle valeur, et la barre et l'anneau indéterminés pulsent sur place au lieu de se déplacer.
  • Les valeurs sont formatées en en-US sauf si vous passez locale, pour que le serveur et le navigateur affichent le même texte.

Construit sur le progress de Base UI. Chaque partie accepte les props de la primitive qu'elle enveloppe.

PropTypePar défaut
valuenull rend la barre indéterminée.
number | null–
min
number0
max
number100
size
"xs" | "sm" | "default" | "lg""default"
variant
"default" | "success" | "warning" | "destructive""default"
formatFormate la valeur. Sans lui, la valeur s'affiche en pourcentage.
Intl.NumberFormatOptions–
locale
Intl.LocalesArgument"en-US"
getAriaValueText
(formattedValue: string, value: number | null) => string–
className
string | (state) => string–
render
ReactElement | (props, state) => ReactElement<div>
AttributDescription
data-slot="progress"La racine.
data-sizeLa taille : xs, sm, default ou lg.
data-variantLa variante de statut.
data-progressingPrésent tant que la valeur est inférieure à max.
data-completePrésent quand la valeur atteint max.
data-indeterminatePrésent quand la valeur est null ou n'est pas un nombre fini.

Nomme la progressbar. Rend un <span> et prend les mêmes attributs d'état que la racine.

PropTypePar défaut
render
ReactElement | (props, state) => ReactElement<span>
AttributDescription
data-slot="progress-label"Le libellé.
PropTypePar défaut
childrenTexte personnalisé. Sans lui, la valeur formatée s'affiche, ou rien tant que l'état est indéterminé.
(formattedValue: string | null, value: number | null) => ReactNode–
render
ReactElement | (props, state) => ReactElement<span>
AttributDescription
data-slot="progress-value"La valeur.

Rendu par <Progress /> et dimensionné par son size. Exporté pour les compositions personnalisées.

AttributDescription
data-slot="progress-track"La piste.
--progress-dir1, ou -1 en droite à gauche, pour que le glissement indéterminé suive le sens de lecture.

Le remplissage. Sa largeur est définie en ligne à partir de la valeur et s'adoucit entre les mises à jour.

AttributDescription
data-slot="progress-indicator"Le remplissage.
PropTypePar défaut
valuenull fait tourner un arc.
number | null–
min
number0
max
number100
size
"sm" | "default" | "lg" | "xl""default"
variant
"default" | "success" | "warning" | "destructive""default"
locale
Intl.LocalesArgument"en-US"
childrenAffiché au centre de l'anneau.
ReactNode–
render
ReactElement | (props, state) => ReactElement<div>
AttributDescription
data-slot="progress-circle"La racine.
data-sizeLa taille : sm, default, lg ou xl.
data-variantLa variante de statut.
data-progressingPrésent tant que la valeur est inférieure à max.
data-completePrésent quand la valeur atteint max.
data-indeterminatePrésent quand la valeur est null ou n'est pas un nombre fini.
--progress-circle-sizeLa largeur et la hauteur de l'anneau.
--progress-strokeL'épaisseur du trait de l'anneau.