Back to Code Bytes
4 min read
React Progress Circle

Description

The ProgressCircle displays the completion percentage in circular form; similar to a progress bar, but as an SVG circle. It accepts a progress prop that should be a number between 0 and 100. Upon 100% completion the background (circle’s fill) color will change to the progressColor.

🏎️ Example

Default

With Center SVG Icon

You can pass a SVG Icon as a child to the ProgressCircle component. The icon will be centered in the circle. The getCenterIconHeightWidth helper fn, will help calculate the height and width of the icon and properly center it in the circle.

With Progress Completed Fill

When the progress circle is 100% completed then the progress circle’s background is filled with the progress color.

🦾 Usage

import { useState } from "react";
import { ProgressCircle, getCenterIconHeightWidth } from "./ProgressCircle";

const [progress, setProgress] = useState(50);
const iconHeightWidth = getCenterIconHeightWidth(180);

const App = () => (
  <ProgressCircle progress={progress} circleHeightWidth={180} strokeWidth={10}>
    <svg
      xmlns="http://www.w3.org/2000/svg"
      height={iconHeightWidth}
      width={iconHeightWidth}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      stroke-width="2"
      stroke-linecap="round"
      stroke-linejoin="round"
      className="lucide lucide-bone"
    >
      <path d="M17 10c.7-.7 1.69 0 2.5 0a2.5 2.5 0 1 0 0-5 .5.5 0 0 1-.5-.5 2.5 2.5 0 1 0-5 0c0 .81.7 1.8 0 2.5l-7 7c-.7.7-1.69 0-2.5 0a2.5 2.5 0 0 0 0 5c.28 0 .5.22.5.5a2.5 2.5 0 1 0 5 0c0-.81-.7-1.8 0-2.5Z" />
    </svg>
  </ProgressCircle>
);

🍎 Code Byte

ProgressCircle.tsx

import React from "react";
import { motion } from "framer-motion";

export interface ProgressCircleProps {
  progress: number;
  circleHeightWidth: number;
  progressColor?: string;
  gaugeColor?: string;
  strokeWidth?: number;
  children?: React.ReactNode;
}

// When you pass a svg icon as child to the ProgressCircle component, this will calculate the height and width of that svg.
export const getCenterIconHeightWidth = (circleWidthHeight: number) =>
  (circleWidthHeight / 2 - 2) * 0.8;

const transition = {
  type: "tween",
  duration: 1,
  ease: "easeInOut",
};

export const ProgressCircle = ({
  progress,
  circleHeightWidth = 80,
  progressColor = "#16a34a",
  gaugeColor = "#E0E0DE",
  strokeWidth = 3,
  children,
}: ProgressCircleProps) => {
  let circleCXY: number;
  const parentRadius = (circleCXY = circleHeightWidth / 2 - 2);

  const circumference = 2 * Math.PI * parentRadius;
  const strokeDashoffset = circumference - (progress / 100) * circumference;

  // multiply by 1.1 to give the svg 10% padding from its container
  const diameter = 2 * parentRadius * 1.1;
  let minX: number;
  // push from the top and push from the left of the svg container
  const minY = (minX = parentRadius * 0.1 * -1);

  const pathLength = progress >= 100 ? (progress + 1) / 100 : progress / 100;

  return (
    <svg
      width={circleHeightWidth}
      height={circleHeightWidth}
      viewBox={`${minX} ${minY} ${diameter} ${diameter}`}
    >
      {/* Animate the fill (background) color when progress reaches 100% by changing the opacity */}
      <motion.circle
        cx={circleCXY}
        cy={circleCXY}
        r={parentRadius}
        stroke="none"
        strokeWidth={1}
        fill={progressColor}
        initial={{ opacity: 0 }}
        animate={{ opacity: progress >= 100 ? 1 : 0 }}
        exit={{ opacity: 0 }}
        transition={transition}
      />
      {/* Once the progress reaches 100%, we change the stroke of the gauge ring to the progress color;
      otherwise, we get the slightest sliver of the gauge color between the progress ring
      and the inner background color fill. */}
      <circle
        cx={circleCXY}
        cy={circleCXY}
        r={parentRadius}
        stroke={progress >= 100 ? progressColor : gaugeColor}
        strokeWidth={strokeWidth}
        fill="none"
      />
      <motion.circle
        cx={circleCXY}
        cy={circleCXY}
        r={parentRadius}
        stroke={progressColor}
        strokeWidth={strokeWidth}
        fill="none"
        strokeDasharray={circumference}
        strokeDashoffset={strokeDashoffset}
        transform={`rotate(-90,${parentRadius},${parentRadius})`}
        initial={{ pathLength: 0 }}
        animate={{ pathLength: pathLength }}
        transition={transition}
      />
      <svg
        x={0.6 * parentRadius}
        y={0.6 * parentRadius}
        width={0.8 * parentRadius}
        height={0.8 * parentRadius}
        aria-hidden={true}
      >
        {children}
      </svg>
    </svg>
  );
};

🪑 Props

PropTypeDescription
progressnumberThe percentage of completion represented by the progress circle, ranging from 0 - 100.
circleHeightWidthnumberThe height and width of progress circle.
progressColorstringThe color applied to the progress portion of the circle.
gaugeColorstringThe “background” color of the non-progress portion of the circle.
strokeWidthnumberThe thickness of the circle stroke.