HextaUI

Progress

Una barra o anillo que muestra cuánto ha avanzado una tarea, se suaviza entre actualizaciones y se desliza mientras el total es desconocido.

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

Añade el componente, los tokens del tema de HextaUI y los componentes de HextaUI de los que depende.

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

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

<Progress /> dibuja su propia pista e indicador después de sus hijos, así que una etiqueta y un valor quedan en una línea sobre la barra. Cada actualización suaviza el relleno desde donde está, de modo que las actualizaciones rápidas se leen como un movimiento fluido y no como saltos.

Progress
├── ProgressLabel
└── ProgressValue

ProgressCircle
└── ProgressValue

Tamaños

xs, sm, default y lg cambian el grosor de la barra. xs es la línea fina que Attachment dibuja a lo largo de su borde inferior.

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>
  )
}

Estado

variant colorea solo el relleno o el anillo, así que la pista, la etiqueta y el valor se mantienen neutros.

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>
  )
}

Indeterminado

Pasa value={null} mientras se desconoce el total. Un segmento se desliza por la pista y, cuando llega un número, el relleno crece desde el inicio.

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>
  )
}

Círculo

<ProgressCircle /> dibuja el mismo valor como un anillo, empezando por arriba. Los hijos quedan en el centro, lo que encaja con <ProgressValue /> en lg y 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>
  )
}

Círculo indeterminado

Un arco gira alrededor del anillo hasta que llega un valor.

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>
  )
}

Rango y formato personalizados

Define min y max para cualquier rango, format para el número y un hijo función en <ProgressValue /> para el texto. Da a los lectores de pantalla las mismas palabras con 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>
  )
}

Valor animado

Renderiza <NumberFlow /> dentro de <ProgressValue /> para que solo giren los dígitos que cambian, al ritmo del relleno.

"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>
  )
}

Etiquetas largas

Los nombres largos se ajustan en sus propias líneas y el valor se queda al final. Los anillos funcionan como estado compacto junto a cada fila.

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>
  )
}

Sin etiqueta visible

Da nombre a la barra con aria-label cuando el contexto ya dice qué se está cargando.

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 derecha a izquierda

El relleno y el deslizamiento indeterminado empiezan desde la derecha. Pasa locale para dar formato al valor con los dígitos del lector.

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 raíz es un progressbar con aria-valuenow, aria-valuemin, aria-valuemax y un aria-valuetext con formato. Mientras es indeterminado no tiene valor actual.
  • <ProgressLabel /> da nombre a la barra. Sin él, pasa aria-label.
  • <ProgressValue /> se oculta a los lectores de pantalla, ya que el progressbar ya anuncia el valor.
  • Con movimiento reducido, el relleno salta a cada nuevo valor, y la barra y el anillo indeterminados pulsan en su sitio en lugar de moverse.
  • Los valores se formatean en en-US salvo que pases locale, para que el servidor y el navegador rendericen el mismo texto.

Construido sobre el progress de Base UI. Cada parte acepta las props de la primitiva que envuelve.

PropTipoPredeterminado
valuenull hace que la barra sea indeterminada.
number | null–
min
number0
max
number100
size
"xs" | "sm" | "default" | "lg""default"
variant
"default" | "success" | "warning" | "destructive""default"
formatDa formato al valor. Sin él, el valor se muestra como porcentaje.
Intl.NumberFormatOptions–
locale
Intl.LocalesArgument"en-US"
getAriaValueText
(formattedValue: string, value: number | null) => string–
className
string | (state) => string–
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescripción
data-slot="progress"La raíz.
data-sizeEl tamaño: xs, sm, default o lg.
data-variantLa variante de estado.
data-progressingPresente mientras el valor está por debajo de max.
data-completePresente cuando el valor alcanza max.
data-indeterminatePresente cuando el valor es null o no es un número finito.

Da nombre al progressbar. Renderiza un <span> y acepta los mismos atributos de estado que la raíz.

PropTipoPredeterminado
render
ReactElement | (props, state) => ReactElement<span>
AtributoDescripción
data-slot="progress-label"La etiqueta.
PropTipoPredeterminado
childrenTexto personalizado. Sin él, se muestra el valor formateado, o nada mientras es indeterminado.
(formattedValue: string | null, value: number | null) => ReactNode–
render
ReactElement | (props, state) => ReactElement<span>
AtributoDescripción
data-slot="progress-value"El valor.

Lo renderiza <Progress /> y su tamaño lo define size. Se exporta para composiciones personalizadas.

AtributoDescripción
data-slot="progress-track"La pista.
--progress-dir1, o -1 en derecha a izquierda, para que el deslizamiento indeterminado siga la dirección de lectura.

El relleno. Su ancho se define en línea a partir del valor y se suaviza entre actualizaciones.

AtributoDescripción
data-slot="progress-indicator"El relleno.
PropTipoPredeterminado
valuenull hace girar un arco.
number | null–
min
number0
max
number100
size
"sm" | "default" | "lg" | "xl""default"
variant
"default" | "success" | "warning" | "destructive""default"
locale
Intl.LocalesArgument"en-US"
childrenSe muestra en el centro del anillo.
ReactNode–
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescripción
data-slot="progress-circle"La raíz.
data-sizeEl tamaño: sm, default, lg o xl.
data-variantLa variante de estado.
data-progressingPresente mientras el valor está por debajo de max.
data-completePresente cuando el valor alcanza max.
data-indeterminatePresente cuando el valor es null o no es un número finito.
--progress-circle-sizeEl ancho y el alto del anillo.
--progress-strokeEl grosor del trazo del anillo.