Skip to content
  1. Keyline UI
  2. Progress
Keyline UIComponents
  • Get started
    • Installation
    • Theming
    • Modes
    • Configuration
    • Composition
  • Actions
    • Button
    • Button group
    • Split button
    • Toggle
    • Toggle group
    • Toolbar
    • Menu
    • Menubar
    • Context menu
  • Inputs
    • Field and form
    • Checkbox
    • Checkbox group
    • Radio group
    • Switch
    • Slider
    • Segmented
    • Select
    • Native select
    • Combobox
    • Multiselect
    • Number field
    • OTP field
    • Time field
    • Date picker and calendar
    • Date range
  • Data
    • Table
    • Chart
    • Stat and meter
    • Status and badge
    • Progress
    • Skeleton
    • Empty
    • Avatar
    • Kbd
    • Marker
  • Navigation
    • Tabs
    • Breadcrumb
    • Pagination
    • Nav
    • Navigation menu
    • Command menu
  • Overlays
    • Dialog
    • Alert dialog
    • Drawer
    • Sheet
    • Popover
    • Hover card
    • Tooltip
    • Toast
    • Notice
  • Layout
    • App shell
    • Sidebar
    • Panel
    • Resizable
    • Scroll area
    • Accordion
    • Collapsible
    • Carousel
    • Inspector
    • Item
    • Direction
  • Conversation
    • Message
    • Bubble
    • Message scroller
    • Attachment
    • Questionnaire
  • Blocks
    • Shipments

Progress

How far a task has got. The fill is the pending yellow with an ink leading edge, the readout gains the done glyph at the end, and a failed task stays where it stopped in red. Say why it failed and what to do in the detail line.

Loading the live example…

"use client"

import { useEffect, useRef, useState } from "react"
import { Button } from "@/components/keyline/ui/button"
import { Progress } from "@/components/keyline/ui/progress"

const SIZE = 4.8 // MB

export function UploadReport() {
  const [sent, setSent] = useState<number | null>(null)
  const [failed, setFailed] = useState(false)
  const tries = useRef(0)
  const timer = useRef<ReturnType<typeof setInterval>>(undefined)
  useEffect(() => () => clearInterval(timer.current), [])

  const upload = () => {
    tries.current += 1
    // the first try drops at 62%, as corporate's server does on Fridays; the retry goes through
    const dropAt = tries.current === 1 ? 62 : Infinity
    setFailed(false)
    setSent(0)
    clearInterval(timer.current)
    timer.current = setInterval(() => {
      setSent((v) => {
        const next = Math.min(100, (v ?? 0) + 7)
        if (next >= dropAt) {
          clearInterval(timer.current)
          setFailed(true)
          return dropAt
        }
        if (next === 100) clearInterval(timer.current)
        return next
      })
    }, 180)
  }

  const mb = sent == null ? 0 : (SIZE * sent) / 100
  const running = sent != null && sent < 100 && !failed

  return (
    <div className="grid max-w-[480px] gap-4">
      <Progress
        label="Q4 sales report.xlsx to corporate"
        value={sent ?? 0}
        failed={failed}
        detail={
          failed
            ? "The connection to Stamford dropped. Nothing was saved there; try again."
            : sent === 100
              ? `${SIZE} MB sent. Jan in New York has it.`
              : sent == null
                ? `${SIZE} MB, not sent yet.`
                : `${mb.toFixed(1)} of ${SIZE} MB · about ${Math.ceil(((100 - sent) / 7) * 0.18)} s left`
        }
      />
      <div className="flex flex-wrap gap-3">
        <Button variant="primary" disabled={running} onClick={upload}>
          {failed ? "Try again" : sent === 100 ? "Send again" : "Send report"}
        </Button>
      </div>
    </div>
  )
}

Installation

npx shadcn@latest add @keyline-ui/progress

Run it where your components.json is. The first time, map the @keyline-ui registry: one command, in Installation.

Usage

import { Progress } from "@/components/keyline/ui/progress"
<Progress
  label="Q4 sales report.xlsx to corporate"
  value={sent}
  failed={dropped}
  detail="2.9 of 4.8 MB · about 3 s left"
/>

Examples

Before there is a figure

value= runs the pending segment along the rail until there is something to count. Here the printer warms up first, then counts pages.

Loading the live example…

"use client"

import { useEffect, useRef, useState } from "react"
import { Button } from "@/components/keyline/ui/button"
import { Progress } from "@/components/keyline/ui/progress"

const PAGES = 40

export function PrintInvoices() {
  // null while the printer warms up: there is no figure to show yet, so the bar is indeterminate
  const [printed, setPrinted] = useState<number | null>(0)
  const [started, setStarted] = useState(false)
  const timers = useRef<ReturnType<typeof setTimeout>[]>([])
  useEffect(() => () => timers.current.forEach(clearTimeout), [])

  const print = () => {
    timers.current.forEach(clearTimeout)
    setStarted(true)
    setPrinted(null)
    timers.current = [setTimeout(() => setPrinted(0), 2000)]
    for (let p = 1; p <= PAGES; p++) timers.current.push(setTimeout(() => setPrinted(p), 2000 + p * 70))
  }

  return (
    <div className="grid max-w-[480px] gap-4">
      <Progress
        label="Invoices on the second-floor printer"
        value={printed}
        max={PAGES}
        getAriaValueText={(_, v) => (v == null ? "Warming up" : `${v} of ${PAGES} pages`)}
        detail={!started ? `${PAGES} pages, waiting to start.` : printed == null ? "Warming up. It takes a while; it always has." : printed === PAGES ? "All 40 printed. Collect them before Creed does." : `${printed} of ${PAGES} pages`}
      />
      <Button className="justify-self-start" disabled={started && printed !== PAGES} onClick={print}>
        {printed === PAGES ? "Print again" : "Print"}
      </Button>
    </div>
  )
}

Props

PropTypeDefaultDescription
labelReactNodeDefault —What is in progress.
valuenumber | nullDefault —How far it has got. null while it can't be counted.
maxnumberDefault 100The value at which it is complete.
detailReactNodeDefault —A line under the rail: amounts and time left, or why it failed and what to do.
failedbooleanDefault falseThe task stopped. value keeps how far it got.
failedTextstringDefault "Failed"The readout when failed.
hideLabelbooleanDefault falseOnly when the text around it already gives the label and figure, as in a table cell.

Under reduced motion the pending segment is hatched and still. Built on Base UI Progress; format, locale and getAriaValueText pass through.

Sheet 36 of 73PreviousStatus and badgeNextSkeleton
On this page