# Progress Bar

> Determinate and indeterminate progress bar with label, percentage, sizes and animated stripes.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/progress-bar
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/progress-bar.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

No extra dependencies.

## Usage

```tsx title="components/progress-bar-demo.tsx"
"use client"

import * as React from "react"

import { ProgressBar } from "@/components/progress-bar"

const btn =
  "inline-flex h-8 items-center rounded-lg border bg-background px-3 text-xs font-medium transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-ring/40"

export default function ProgressBarDemo() {
  const [value, setValue] = React.useState(35)

  return (
    <div className="flex w-full max-w-md flex-col gap-6">
      <div className="flex flex-col gap-3">
        <ProgressBar label="Uploading assets" value={value} />
        <div className="flex gap-2">
          <button type="button" className={btn} onClick={() => setValue((v) => Math.max(0, v - 15))}>
            -15
          </button>
          <button type="button" className={btn} onClick={() => setValue((v) => Math.min(100, v + 15))}>
            +15
          </button>
        </div>
      </div>
      <ProgressBar label="Striped" value={68} striped size="lg" />
      <ProgressBar label="Storage" value={92} size="md" barClassName="bg-amber-500" />
      <ProgressBar label="Completed" value={100} size="sm" barClassName="bg-emerald-500" />
      <ProgressBar label="Syncing..." />
    </div>
  )
}
```

## Source

```tsx title="components/progress-bar.tsx"
import * as React from "react"

import { cn } from "@/lib/utils"

export interface ProgressBarProps extends Omit<React.ComponentProps<"div">, "children"> {
  /** Current value. Omit (or pass `null`) for an indeterminate bar. */
  value?: number | null
  max?: number
  size?: "sm" | "md" | "lg"
  label?: React.ReactNode
  /** Show the percentage next to the label. Ignored when indeterminate. */
  showValue?: boolean
  /** Animated diagonal stripes on the fill. */
  striped?: boolean
  /** Fill color classes, e.g. "bg-emerald-500". */
  barClassName?: string
}

const heights = { sm: "h-1.5", md: "h-2.5", lg: "h-4" } as const

const css = `
@keyframes atelier-pb-stripes{from{background-position:1rem 0}to{background-position:0 0}}
@keyframes atelier-pb-slide{0%{transform:translateX(-100%)}100%{transform:translateX(250%)}}
.atelier-pb-striped{background-image:linear-gradient(135deg,rgba(255,255,255,.28) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.28) 50%,rgba(255,255,255,.28) 75%,transparent 75%,transparent);background-size:1rem 1rem;animation:atelier-pb-stripes .8s linear infinite}
.atelier-pb-slide{width:40%;animation:atelier-pb-slide 1.4s cubic-bezier(.4,0,.2,1) infinite}
@media (prefers-reduced-motion:reduce){.atelier-pb-striped{animation-duration:3.2s}.atelier-pb-slide{animation-duration:4.2s}}
`

export function ProgressBar({
  value = null,
  max = 100,
  size = "md",
  label,
  showValue = true,
  striped = false,
  barClassName,
  className,
  ...props
}: ProgressBarProps) {
  const indeterminate = value === null || value === undefined
  const clamped = indeterminate ? 0 : Math.min(Math.max(value, 0), max)
  const percent = max > 0 ? Math.round((clamped / max) * 100) : 0

  return (
    <div className={cn("w-full", className)} {...props}>
      <style href="atelier-progress-bar" precedence="default">
        {css}
      </style>
      {(label || (showValue && !indeterminate)) && (
        <div className="mb-1.5 flex items-center justify-between text-sm">
          <span className="font-medium">{label}</span>
          {showValue && !indeterminate && (
            <span className="tabular-nums text-muted-foreground">{percent}%</span>
          )}
        </div>
      )}
      <div
        role="progressbar"
        aria-label={typeof label === "string" ? label : "Progress"}
        aria-valuemin={0}
        aria-valuemax={max}
        aria-valuenow={indeterminate ? undefined : clamped}
        aria-valuetext={indeterminate ? "Loading" : `${percent}%`}
        className={cn("relative w-full overflow-hidden rounded-full bg-muted", heights[size])}
      >
        {indeterminate ? (
          <div className={cn("atelier-pb-slide absolute inset-y-0 left-0 rounded-full bg-primary", barClassName)} />
        ) : (
          <div
            className={cn(
              "h-full rounded-full bg-primary transition-[width] duration-500 ease-out",
              striped && "atelier-pb-striped",
              barClassName
            )}
            style={{ width: `${(clamped / (max || 1)) * 100}%` }}
          />
        )}
      </div>
    </div>
  )
}
```
