Dice UI

Circular Progress

Component 5

About

A composable circular progress indicator with track, range, and value text parts, supporting determinate and indeterminate states.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Circular Progress to your project and fits it to your theme.

Code

How Circular Progress is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client";

import {
  CircularProgress,
  CircularProgressIndicator,
  CircularProgressRange,
  CircularProgressTrack,
  CircularProgressValueText,
} from "@/components/ui/circular-progress";
import { motion, useInView, useMotionValue, useSpring } from "motion/react";
import * as React from "react";
import { cn } from "@/lib/utils";

const themes = [
  {
    name: "Default",
    trackClass: "text-muted-foreground/20",
    rangeClass: "text-primary",
    textClass: "text-foreground",
  },
  {
    name: "Success",
    trackClass: "text-green-200 dark:text-green-900",
    rangeClass: "text-green-500",
    textClass: "text-green-700 dark:text-green-300",
  },
  {
    name: "Warning",
    trackClass: "text-yellow-200 dark:text-yellow-900",
    rangeClass: "text-yellow-500",
    textClass: "text-yellow-700 dark:text-yellow-300",
  },
  {
    name: "Destructive",
    trackClass: "text-red-200 dark:text-red-900",
    rangeClass: "text-red-500",
    textClass: "text-red-700 dark:text-red-300",
  },
  {
    name: "Purple",
    trackClass: "text-purple-200 dark:text-purple-900",
    rangeClass: "text-purple-500",
    textClass: "text-purple-700 dark:text-purple-300",
  },
  {
    name: "Orange",
    trackClass: "text-orange-200 dark:text-orange-900",
    rangeClass: "text-orange-500",
    textClass: "text-orange-700 dark:text-orange-300",
  },
  {
    name: "Blue",
    trackClass: "text-blue-200 dark:text-blue-900",
    rangeClass: "text-blue-500",
    textClass: "text-blue-700 dark:text-blue-300",
  },
  {
    name: "Pink",
    trackClass: "text-pink-200 dark:text-pink-900",
    rangeClass: "text-pink-500",
    textClass: "text-pink-700 dark:text-pink-300",
  },
];

export default function CircularProgressColorsDemo() {
  return (
    <>
      <div className="hidden grid-cols-4 gap-4 sm:grid">
        {themes.map((theme, index) => (
          <AnimatedCircularProgress
            key={theme.name}
            theme={theme}
            index={index}
          />
        ))}
      </div>
      <div className="grid grid-cols-2 gap-4 sm:hidden">
        {themes.slice(0, 4).map((theme, index) => (
          <AnimatedCircularProgress
            key={theme.name}
            theme={theme}
            index={index}
          />
        ))}
      </div>
    </>
  );
}

interface AnimatedCircularProgressProps {
  theme: (typeof themes)[0];
  index: number;
}

function AnimatedCircularProgress({
  theme,
  index,
}: AnimatedCircularProgressProps) {
  const ref = React.useRef(null);
  const isInView = useInView(ref, { once: true, margin: "-100px" });

  const motionValue = useMotionValue(0);
  const springValue = useSpring(motionValue, {
    stiffness: 60,
    damping: 15,
    mass: 1,
  });

  const [displayValue, setDisplayValue] = React.useState(0);

  React.useEffect(() => {
    if (isInView) {
      const delay = index * 150;
      const timer = setTimeout(() => {
        motionValue.set(75);
      }, delay);

      return () => clearTimeout(timer);
    }
  }, [isInView, motionValue, index]);

  React.useEffect(() => {
    const unsubscribe = springValue.on("change", (latest) => {
      setDisplayValue(Math.round(latest));
    });

    return unsubscribe;
  }, [springValue]);

  return (
    <motion.div
      ref={ref}
      className="flex flex-col items-center gap-3"
      initial={{ opacity: 0, y: 20 }}
      animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
      transition={{
        duration: 0.6,
        delay: index * 0.1,
        ease: [0.21, 1.11, 0.81, 0.99],
      }}
    >
      <CircularProgress value={displayValue} size={80} thickness={6}>
        <CircularProgressIndicator>
          <CircularProgressTrack className={theme.trackClass} />
          <CircularProgressRange className={theme.rangeClass} />
        </CircularProgressIndicator>
        <CircularProgressValueText
          className={cn("font-semibold text-sm", theme.textClass)}
        />
      </CircularProgress>
      <div className="flex flex-col items-center gap-1 text-center">
        <h4 className="font-medium text-sm">{theme.name}</h4>
        <p className="text-muted-foreground text-xs">
          {displayValue}% complete
        </p>
      </div>
    </motion.div>
  );
}

Dependencies

More by Dice UI

View all
  • Compare Slider

    Compare Slider

    75
  • Sortable

    Sortable

    49
  • Status

    Status

    28
  • Pending

    Pending

    27
  • Banner

    Banner

    19
  • Stack

    Stack

    8
  • Scroller

    Scroller

    5
  • Kanban Board

    Kanban Board

    4

Explore more

Part of Dice UI — browse the full library on 21st.dev.