Components

Progress Bar

A progress bar with label, percentage readout, semantic color variants, three sizes, and an indeterminate state.

feedback

Installation

npx cano-ui add progress-bar

Installs @radix-ui/react-progress automatically if missing.

Source

"use client"

import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"

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

export type ProgressBarVariant = "default" | "success" | "warning" | "danger"
export type ProgressBarSize = "sm" | "default" | "lg"

export interface ProgressBarProps {
  /** 0–100. Omit (undefined) for an indeterminate loading bar. */
  value?: number
  label?: string
  /** Shows the percentage next to the label. */
  showValue?: boolean
  variant?: ProgressBarVariant
  size?: ProgressBarSize
  className?: string
}

const variantClasses: Record<ProgressBarVariant, string> = {
  default: "bg-primary",
  success: "bg-emerald-500",
  warning: "bg-amber-500",
  danger: "bg-destructive",
}

const sizeClasses: Record<ProgressBarSize, string> = {
  sm: "h-1",
  default: "h-2",
  lg: "h-3",
}

export function ProgressBar({
  value,
  label,
  showValue = false,
  variant = "default",
  size = "default",
  className,
}: ProgressBarProps) {
  const indeterminate = value === undefined
  const clamped = indeterminate ? 0 : Math.min(100, Math.max(0, value))

  return (
    <div
      data-slot="progress-bar"
      className={cn("flex w-full flex-col gap-1.5", className)}
    >
      {label || showValue ? (
        <div className="flex items-baseline justify-between gap-2">
          {label ? (
            <span className="text-sm font-medium text-foreground">{label}</span>
          ) : null}
          {showValue && !indeterminate ? (
            <span className="text-xs tabular-nums text-muted-foreground">
              {Math.round(clamped)}%
            </span>
          ) : null}
        </div>
      ) : null}
      <ProgressPrimitive.Root
        value={indeterminate ? null : clamped}
        aria-label={label}
        className={cn(
          "relative w-full overflow-hidden rounded-full bg-muted",
          sizeClasses[size]
        )}
      >
        <ProgressPrimitive.Indicator
          className={cn(
            "h-full rounded-full transition-transform duration-500 ease-out",
            variantClasses[variant],
            indeterminate &&
              "w-1/3 animate-[progress-bar-slide_1.2s_ease-in-out_infinite]"
          )}
          style={
            indeterminate
              ? undefined
              : { transform: `translateX(-${100 - clamped}%)` }
          }
        />
      </ProgressPrimitive.Root>
      {indeterminate ? (
        <style>{`@keyframes progress-bar-slide { 0% { transform: translateX(-100%); } 100% { transform: translateX(400%); } }`}</style>
      ) : null}
    </div>
  )
}

This exact file lands in your project at components/ui/progress-bar.tsx. It is yours to edit.