Build websites 10x faster with HextaUI Blocks — Learn more
UI/UI

Progress

A versatile progress component for displaying completion status, loading states, and step-by-step processes.

Loading...
Download Progress
65%
Processing
65%
65%
<div className="space-y-8 w-full max-w-md">
  <Progress value={65} label="Loading..." className="w-full" />
  <Progress
    value={65}
    label="Download Progress"
    showValue
    className="w-full"
  />
  <div className="flex justify-center">
    <Progress value={65} label="Processing" type="circular" showValue />
  </div>
</div>

Installation

Install following dependencies:

npm install @radix-ui/react-progress class-variance-authority motion
pnpm add @radix-ui/react-progress class-variance-authority motion
yarn add @radix-ui/react-progress class-variance-authority motion
bun add @radix-ui/react-progress class-variance-authority motion

Copy and paste the following code into your project.

components/ui/progress.tsx
"use client";

import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { motion } from "motion/react";

const progressVariants = cva(
  "relative overflow-hidden rounded-full bg-[hsl(var(--hu-secondary))]",
  {
    variants: {
      variant: {
        default: "bg-[hsl(var(--hu-secondary))]",
        primary: "bg-[hsl(var(--hu-primary))]/10",
        secondary: "bg-[hsl(var(--hu-secondary))]",
        destructive: "bg-[hsl(var(--hu-destructive))]/10",
        outline:
          "bg-[hsl(var(--hu-accent))] border border-[hsl(var(--hu-border))]",
      },
      size: {
        sm: "h-1.5",
        default: "h-2.5",
        lg: "h-3",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  },
);

const progressIndicatorVariants = cva(
  "h-full w-full flex-1 rounded-full transition-all duration-500 ease-out",
  {
    variants: {
      variant: {
        default: "bg-[hsl(var(--hu-primary))]",
        primary: "bg-[hsl(var(--hu-primary))]",
        secondary: "bg-[hsl(var(--hu-foreground))]",
        destructive: "bg-[hsl(var(--hu-destructive))]",
        outline: "bg-[hsl(var(--hu-primary))]",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  },
);

const circularProgressVariants = cva(
  "relative flex items-center justify-center",
  {
    variants: {
      size: {
        sm: "w-12 h-12",
        default: "w-16 h-16",
        lg: "w-20 h-20",
      },
    },
    defaultVariants: {
      size: "default",
    },
  },
);

export interface ProgressProps
  extends React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>,
    VariantProps<typeof progressVariants> {
  value?: number;
  showValue?: boolean;
  animated?: boolean;
  type?: "linear" | "circular";
  strokeWidth?: number;
  label?: string;
}

const Progress = React.forwardRef<
  React.ElementRef<typeof ProgressPrimitive.Root>,
  ProgressProps
>(
  (
    {
      className,
      value = 0,
      variant,
      size,
      showValue = false,
      animated = true,
      type = "linear",
      strokeWidth,
      label,
      ...props
    },
    ref,
  ) => {
    const progress = Math.min(Math.max(value, 0), 100);

    if (type === "circular") {
      const circleSize = size === "sm" ? 48 : size === "lg" ? 80 : 64;
      const radius = (circleSize - (strokeWidth || 8)) / 2;
      const circumference = 2 * Math.PI * radius;
      const strokeDashoffset = circumference - (progress / 100) * circumference;
      return (
        <div className="space-y-2">
          {label && (
            <div className="text-sm font-medium text-[hsl(var(--hu-foreground))]">
              {label}
            </div>
          )}
          <div className={cn(circularProgressVariants({ size }), className)}>
            <svg
              width={circleSize}
              height={circleSize}
              className="transform -rotate-90"
            >
              {/* Background circle */}
              <circle
                cx={circleSize / 2}
                cy={circleSize / 2}
                r={radius}
                stroke="hsl(var(--hu-secondary))"
                strokeWidth={strokeWidth || 8}
                fill="transparent"
                className="opacity-20"
              />
              {/* Progress circle */}
              <motion.circle
                cx={circleSize / 2}
                cy={circleSize / 2}
                r={radius}
                stroke={
                  variant === "destructive"
                    ? "hsl(var(--hu-destructive))"
                    : variant === "secondary"
                      ? "hsl(var(--hu-secondary-foreground))"
                      : variant === "outline"
                        ? "hsl(var(--hu-foreground))"
                        : "hsl(var(--hu-primary))"
                }
                strokeWidth={strokeWidth || 8}
                fill="transparent"
                strokeLinecap="round"
                strokeDasharray={circumference}
                initial={{ strokeDashoffset: circumference }}
                animate={{
                  strokeDashoffset: animated
                    ? strokeDashoffset
                    : strokeDashoffset,
                }}
                transition={{
                  duration: animated ? 1.5 : 0,
                  ease: "easeInOut",
                }}
              />
            </svg>
            {showValue && (
              <motion.div
                className="absolute inset-0 flex items-center justify-center text-sm font-semibold tabular-nums"
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                transition={{ delay: animated ? 0.5 : 0, duration: 0.3 }}
              >
                {Math.round(progress)}%
              </motion.div>
            )}
          </div>
          {showValue && (
            <motion.div
              className="text-center text-xs font-medium text-[hsl(var(--hu-muted-foreground))] tabular-nums"
              initial={{ opacity: 0, y: -5 }}
              animate={{ opacity: 1, y: 0 }}
              transition={{ delay: animated ? 0.3 : 0, duration: 0.2 }}
            >
              {Math.round(progress)}%
            </motion.div>
          )}
        </div>
      );
    }
    return (
      <div className="space-y-2">
        {label && (
          <div className="text-sm font-medium text-[hsl(var(--hu-foreground))]">
            {label}
          </div>
        )}
        <ProgressPrimitive.Root
          ref={ref}
          className={cn(progressVariants({ variant, size }), className)}
          {...props}
        >
          <ProgressPrimitive.Indicator
            className={cn(progressIndicatorVariants({ variant }))}
            asChild
          >
            <motion.div
              initial={{ transform: "translateX(-100%)" }}
              animate={{ transform: `translateX(-${100 - progress}%)` }}
              transition={{
                duration: animated ? 1.2 : 0,
                ease: "easeInOut",
              }}
            />
          </ProgressPrimitive.Indicator>
        </ProgressPrimitive.Root>
        {showValue && (
          <motion.div
            className="text-right text-xs font-semibold text-[hsl(var(--hu-muted-foreground))] tabular-nums"
            initial={{ opacity: 0, y: -5 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{ delay: animated ? 0.3 : 0, duration: 0.2 }}
          >
            {Math.round(progress)}%
          </motion.div>
        )}
      </div>
    );
  },
);

Progress.displayName = ProgressPrimitive.Root.displayName;

export { Progress, progressVariants };
npx hextaui@latest add progress
pnpm dlx hextaui@latest add progress
yarn dlx hextaui@latest add progress
bun x hextaui@latest add progress

Usage

import { Progress } from "@/components/ui/Progress/Progress";
<Progress value={65} label="Loading..." />
<Progress value={65} label="Download Progress" showValue />
<Progress value={65} label="Processing" type="circular" showValue />

Examples

Variants

Default Variant
75%
Primary Variant
60%
Secondary Variant
45%
Destructive Variant
30%
Outline Variant
85%
<div className="space-y-6 w-full max-w-md">
  <Progress
    value={75}
    label="Default Variant"
    variant="default"
    showValue
    className="w-full"
  />
  <Progress
    value={60}
    label="Primary Variant"
    variant="primary"
    showValue
    className="w-full"
  />
  <Progress
    value={45}
    label="Secondary Variant"
    variant="secondary"
    showValue
    className="w-full"
  />
  <Progress
    value={30}
    label="Destructive Variant"
    variant="destructive"
    showValue
    className="w-full"
  />
  <Progress
    value={85}
    label="Outline Variant"
    variant="outline"
    showValue
    className="w-full"
  />
</div>

Sizes

Small Progress
Default Progress
Large Progress
<div className="space-y-8 w-full max-w-md">
  <Progress
    value={65}
    label="Small Progress"
    size="sm"
    className="w-full"
  />
  <Progress
    value={65}
    label="Default Progress"
    size="default"
    className="w-full"
  />
  <Progress
    value={65}
    label="Large Progress"
    size="lg"
    className="w-full"
  />
</div>

Circular Progress

Small

50%
50%

Default

75%
75%

Large

<div className="flex gap-8 items-center justify-center">
  <div className="text-center space-y-3">
    <Progress value={25} type="circular" size="sm" />
    <div className="text-xs text-muted-foreground">Small</div>
  </div>
  <div className="text-center space-y-3">
    <Progress value={50} type="circular" size="default" showValue />
    <div className="text-xs text-muted-foreground">Default</div>
  </div>
  <div className="text-center space-y-3">
    <Progress
      value={75}
      type="circular"
      size="lg"
      showValue
      variant="destructive"
    />
    <div className="text-xs text-muted-foreground">Large</div>
  </div>
</div>

Animated Progress

File Upload
0%
Processing
0%
0%
import { useState, useEffect } from "react";
import { Progress } from "@/components/ui/Progress";

function AnimatedProgressExample() {
  const [progress, setProgress] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setProgress((prev) => {
        if (prev >= 100) {
          return 0;
        }
        return prev + 10;
      });
    }, 600);

    return () => clearInterval(interval);
  }, []);

  return (
    <div className="space-y-4">
      <Progress value={progress} showValue animated />
      <Progress value={progress} type="circular" showValue animated />
    </div>
  );
}

Loading States

Uploading files...

3 of 5

Installing packages

85%

Build failed

Error

<div className="space-y-8 w-full max-w-md">
  <div className="space-y-3">
    <div className="flex justify-between items-center text-sm">
      <span className="font-medium">Uploading files...</span>
      <span className="text-muted-foreground text-xs">3 of 5</span>
    </div>
    <Progress value={60} className="w-full" />
  </div>
  <div className="space-y-3">
    <div className="flex justify-between items-center text-sm">
      <span className="font-medium">Installing packages</span>
      <span className="text-muted-foreground text-xs">85%</span>
    </div>
    <Progress value={85} variant="secondary" className="w-full" />
  </div>
  <div className="space-y-3">
    <div className="flex justify-between items-center text-sm">
      <span className="font-medium">Build failed</span>
      <span className="text-destructive text-xs font-medium">Error</span>
    </div>
    <Progress value={45} variant="destructive" className="w-full" />
  </div>
</div>

Step Progress

PlanningDevelopmentTestingDeployment
Planning - Step 1 of 4
25%
Planning
import { useState } from "react";
import { Progress } from "@/components/ui/Progress";

function ProgressWithSteps() {
  const [currentStep, setCurrentStep] = useState(0);
  const steps = ["Planning", "Development", "Testing", "Deployment"];
  const progress = ((currentStep + 1) / steps.length) * 100;

  return (
    <div className="space-y-4">
      <div className="flex justify-between text-sm">
        {steps.map((step, index) => (
          <span
            key={step}
            className={`${
              index <= currentStep
                ? "text-primary font-medium"
                : "text-muted-foreground"
            }`}
          >
            {step}
          </span>
        ))}
      </div>
      <Progress value={progress} />
      <div className="flex gap-2">
        <button
          onClick={() => setCurrentStep(Math.max(0, currentStep - 1))}
          disabled={currentStep === 0}
          className="px-3 py-1 text-sm bg-secondary text-secondary-foreground rounded disabled:opacity-50"
        >
          Previous
        </button>
        <button
          onClick={() =>
            setCurrentStep(Math.min(steps.length - 1, currentStep + 1))
          }
          disabled={currentStep === steps.length - 1}
          className="px-3 py-1 text-sm bg-primary text-primary-foreground rounded disabled:opacity-50"
        >
          Next
        </button>
      </div>
    </div>
  );
}

Custom Stroke Width

70%
70%
70%
70%
70%
70%
<div className="flex gap-8 items-center">
  <Progress
    value={70}
    type="circular"
    strokeWidth={4}
    size="default"
    showValue
  />
  <Progress
    value={70}
    type="circular"
    strokeWidth={8}
    size="default"
    showValue
  />
  <Progress
    value={70}
    type="circular"
    strokeWidth={12}
    size="lg"
    showValue
  />
</div>

Without Animation

75%
75%
75%
<div className="space-y-4 w-full max-w-md">
  <Progress value={75} animated={false} showValue />
  <Progress value={75} type="circular" animated={false} showValue />
</div>

Real-world Examples

Profile Completion

4 of 5 sections completed

Storage Usage7.2 GB of 10 GB
Download ProgressDownloading...
45%
<div className="space-y-8 w-full max-w-md">
  <div className="space-y-4">
    <div className="flex items-center justify-between">
      <span className="text-sm font-semibold">Profile Completion</span>
      <Progress value={80} type="circular" size="sm" />
    </div>
    <Progress value={80} size="sm" className="w-full" />
    <div className="text-xs text-muted-foreground text-center">
      4 of 5 sections completed
    </div>
  </div>
  
  <div className="space-y-3">
    <div className="flex items-center justify-between">
      <span className="text-sm font-semibold">Storage Usage</span>
      <span className="text-xs text-muted-foreground font-medium">7.2 GB of 10 GB</span>
    </div>
    <Progress value={72} variant="secondary" className="w-full" />
  </div>

  <div className="space-y-3">
    <div className="flex items-center justify-between">
      <span className="text-sm font-semibold">Download Progress</span>
      <span className="text-xs text-muted-foreground font-medium">Downloading...</span>
    </div>
    <Progress value={45} showValue className="w-full" />
  </div>
</div>

Props

PropTypeDefault
className?
string
undefined
label?
string
undefined
strokeWidth?
number
8
animated?
boolean
true
showValue?
boolean
false
type?
"linear" | "circular"
"linear"
size?
"sm" | "default" | "lg"
"default"
variant?
"default" | "primary" | "secondary" | "destructive" | "outline"
"default"
value?
number
0
Edit on GitHub

Last updated on