HextaUI

Progress

Ein Balken oder Ring, der zeigt, wie weit eine Aufgabe fortgeschritten ist, sanft zwischen Aktualisierungen wechselt und gleitet, solange die Gesamtmenge unbekannt ist.

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

Fügt die Komponente, die HextaUI-Theme-Tokens und alle HextaUI-Komponenten hinzu, von denen sie abhängt.

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

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

<Progress /> zeichnet seine eigene Spur und seinen Indikator nach seinen Kindern, sodass ein Label und ein Wert in einer Zeile über der Leiste stehen. Jedes Update lässt die Füllung von der aktuellen Stelle auslaufen, sodass schnelle Updates als eine fließende Bewegung statt als Stufen wirken.

Progress
├── ProgressLabel
└── ProgressValue

ProgressCircle
└── ProgressValue

Größen

xs, sm, default und lg ändern die Dicke der Leiste. xs ist die feine Linie, die Attachment an seinem unteren Rand zeichnet.

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

Status

variant färbt nur die Füllung oder den Ring, sodass Spur, Label und Wert neutral bleiben.

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

Unbestimmt

Übergib value={null}, solange die Gesamtmenge unbekannt ist. Ein Segment gleitet über die Spur, und sobald eine Zahl eintrifft, wächst die Füllung von Anfang an.

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

Kreis

<ProgressCircle /> zeichnet denselben Wert als Ring, oben beginnend. Kinder sitzen in der Mitte, was zu <ProgressValue /> bei lg und xl passt.

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

Unbestimmter Kreis

Ein Bogen dreht sich um den Ring, bis ein Wert eintrifft.

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

Eigener Bereich und Format

Setze min und max für einen beliebigen Bereich, format für die Zahl und ein Funktions-Child an <ProgressValue /> für den Text. Gib Screenreadern mit getAriaValueText dieselben Worte.

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

Animierter Wert

Rendere <NumberFlow /> in <ProgressValue />, damit sich nur die Ziffern drehen, die sich ändern, im Takt mit der Füllung.

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

Lange Labels

Lange Namen brechen in eigene Zeilen um, und der Wert bleibt am Ende. Ringe funktionieren als kompakter Status neben jeder Zeile.

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

Ohne sichtbares Label

Benenne die Leiste mit aria-label, wenn der Kontext schon sagt, was lädt.

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

Rechts nach links

Füllung und unbestimmtes Gleiten starten von rechts. Übergib locale, um den Wert in den Ziffern des Lesers zu formatieren.

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>
  )
}
  • Das Root ist eine progressbar mit aria-valuenow, aria-valuemin, aria-valuemax und einem formatierten aria-valuetext. Im unbestimmten Zustand hat sie keinen aktuellen Wert.
  • <ProgressLabel /> benennt die Leiste. Ohne eines übergib aria-label.
  • <ProgressValue /> ist für Screenreader ausgeblendet, da die Progressbar den Wert bereits ansagt.
  • Bei reduzierter Bewegung springt die Füllung auf jeden neuen Wert, und die unbestimmte Leiste und der Ring pulsieren an Ort und Stelle, statt sich zu bewegen.
  • Werte werden in en-US formatiert, sofern du kein locale übergibst, damit Server und Browser denselben Text rendern.

Gebaut auf dem Base UI Progress. Jeder Teil akzeptiert die Props des Primitivs, das er umschließt.

PropTypStandard
valuenull macht die Leiste unbestimmt.
number | null–
min
number0
max
number100
size
"xs" | "sm" | "default" | "lg""default"
variant
"default" | "success" | "warning" | "destructive""default"
formatFormatiert den Wert. Ohne das erscheint der Wert als Prozentzahl.
Intl.NumberFormatOptions–
locale
Intl.LocalesArgument"en-US"
getAriaValueText
(formattedValue: string, value: number | null) => string–
className
string | (state) => string–
render
ReactElement | (props, state) => ReactElement<div>
AttributBeschreibung
data-slot="progress"Das Root.
data-sizeDie Größe: xs, sm, default oder lg.
data-variantDie Status-Variante.
data-progressingVorhanden, solange der Wert unter max liegt.
data-completeVorhanden, wenn der Wert max erreicht.
data-indeterminateVorhanden, wenn der Wert null oder keine endliche Zahl ist.

Benennt die Progressbar. Rendert ein <span> und nimmt dieselben State-Attribute wie das Root.

PropTypStandard
render
ReactElement | (props, state) => ReactElement<span>
AttributBeschreibung
data-slot="progress-label"Das Label.
PropTypStandard
childrenEigener Text. Ohne ihn erscheint der formatierte Wert, bei unbestimmtem Zustand nichts.
(formattedValue: string | null, value: number | null) => ReactNode–
render
ReactElement | (props, state) => ReactElement<span>
AttributBeschreibung
data-slot="progress-value"Der Wert.

Wird von <Progress /> gerendert und über dessen size dimensioniert. Für eigene Kompositionen exportiert.

AttributBeschreibung
data-slot="progress-track"Die Spur.
--progress-dir1 oder -1 in rechts-nach-links, sodass das unbestimmte Gleiten der Leserichtung folgt.

Die Füllung. Ihre Breite wird inline aus dem Wert gesetzt und läuft zwischen Updates sanft aus.

AttributBeschreibung
data-slot="progress-indicator"Die Füllung.
PropTypStandard
valuenull dreht einen Bogen.
number | null–
min
number0
max
number100
size
"sm" | "default" | "lg" | "xl""default"
variant
"default" | "success" | "warning" | "destructive""default"
locale
Intl.LocalesArgument"en-US"
childrenWird in der Mitte des Rings angezeigt.
ReactNode–
render
ReactElement | (props, state) => ReactElement<div>
AttributBeschreibung
data-slot="progress-circle"Das Root.
data-sizeDie Größe: sm, default, lg oder xl.
data-variantDie Status-Variante.
data-progressingVorhanden, solange der Wert unter max liegt.
data-completeVorhanden, wenn der Wert max erreicht.
data-indeterminateVorhanden, wenn der Wert null oder keine endliche Zahl ist.
--progress-circle-sizeBreite und Höhe des Rings.
--progress-strokeDie Strichstärke des Rings.