DEV Community

Cover image for Making a progress circle in React
Jack
Jack

Posted on

Making a progress circle in React

When I need something simple that I don't know how to build quickly, it can be tempting to find a library for it. But what is actually going on inside any given library? And how do I make it do this specific thing, instead of the 20 other options this library comes packaged with? I think it can sometimes take longer to read the docs and find workarounds than to just build the thing from scratch, and then at the end it's lighter and easier to make adjustments.

I recently wanted to make a progress circle / pie. It came out looking something like this:

complete progress circle

Here is how I did it in React. The aim of this article is for people to do things their own way rather than using a component someone else has built for them, so I'd love to know what you'd do differently or additionally in the comments.

ideas

SVGs

I like SVGs a lot, they're awesome. They seem to have every attribute you could want and, unlike some CSS properties, they work on all main browsers. So actually we can do this whole thing without CSS. Since this is a dynamic component which will take a percentage value, we'll be using JavaScript to do all the calculations.

We're going to make two circles, one (blue) on top of the other (light grey). We'll use SVG's stroke-dasharray, which sets the length of a dashed stroke, and stroke-dashoffset, which sets where it begins relative to its natural starting point.

So the stroke dash length will be the circumference of the circle - 2 * pi * radius - and the offset which we need to change for the blue circle will be a percentage of that. When we want to visualise 85%, that stroke will have to start at 15% of whatever the circumference is, so that we can only see the remaining 85% of the line before the dash ends.

<svg width="200" height="200">
  <circle r="70" cx="100" cy="100" fill="transparent" stroke="lightgrey" stroke-width="2rem" stroke-dasharray="439.8" stroke-dashoffset="0"></circle>
  <circle r="70" cx="100" cy="100" fill="transparent" stroke="blue" stroke-width="2rem" stroke-dasharray="439.8" stroke-dashoffset="66"></circle>
</svg>
Enter fullscreen mode Exit fullscreen mode

We're already off to a flying start, with some hard-coded values - in particular, the radius of 70, the circumference of 439.8 and its "85%" bar starting at 66. If you try this yourself, you'll see we're 90 degrees clockwise of where we'd want to be, and also missing the text value that should sit neatly in the middle. So we can put the circles in a group to rotate it -90 degrees, and add some text.

<svg width="200" height="200">
  <g transform="rotate(-90 100 100)">
    <circle r="70" cx="100" cy="100" fill="transparent" stroke="lightgrey" stroke-width="2rem" stroke-dasharray="439.8" stroke-dashoffset="0"></circle>
    <circle r="70" cx="100" cy="100" fill="transparent" stroke="blue" stroke-width="2rem" stroke-dasharray="439.8" stroke-dashoffset="66"> 
    </circle>
  </g>
  <text x="50%" y="50%" dominant-baseline="central" text-anchor="middle">85%</text>
</svg>
Enter fullscreen mode Exit fullscreen mode

There are some of those great SVG attributes I mentioned earlier - dominant-baseline and text-anchor helping us centre our text vertically and horizontally. Doing stuff like this in CSS can be a bit of a headache. When rotating SVGs we can also specify the centre of rotation - in this case its in the middle at 100 100.

This actually already gives us the progress circle at the top of the article, so we're ready to move this to React.

Making it a component

Using React gives us a lot of dynamic control over the values we're using. Let's take the percentage we want as an input, and the colour we want the progress to be.

We'll start by 'cleaning' the input to make sure it's a number we can use, we can set up the SVG parts as re-useable components and then we're basically done.

const cleanPercentage = (percentage) => {
  const isNegativeOrNaN = !Number.isFinite(+percentage) || percentage < 0; // we can set non-numbers to 0 here
  const isTooHigh = percentage > 100;
  return isNegativeOrNaN ? 0 : isTooHigh ? 100 : +percentage;
};

const Circle = ({ colour, percentage }) => {
  const r = 70;
  const circ = 2 * Math.PI * r;
  const strokePct = ((100 - percentage) * circ) / 100; // where stroke will start, e.g. from 15% to 100%.
  return (
    <circle
      r={r}
      cx={100}
      cy={100}
      fill="transparent"
      stroke={strokePct !== circ ? colour : ""} // remove colour as 0% sets full circumference
      strokeWidth={"2rem"}
      strokeDasharray={circ}
      strokeDashoffset={percentage ? strokePct : 0}
    ></circle>
  );
};

const Text = ({ percentage }) => {
  return (
    <text
      x="50%"
      y="50%"
      dominantBaseline="central"
      textAnchor="middle"
      fontSize={"1.5em"}
    >
      {percentage.toFixed(0)}%
    </text>
  );
};

const Pie = ({ percentage, colour }) => {
  const pct = cleanPercentage(percentage);
  return (
    <svg width={200} height={200}>
      <g transform={`rotate(-90 ${"100 100"})`}>
        <Circle colour="lightgrey" />
        <Circle colour={colour} percentage={pct} />
      </g>
      <Text percentage={pct} />
    </svg>
  );
};
Enter fullscreen mode Exit fullscreen mode

And actually this is just a starting point, since there are still hard-coded values - do we want to fix our radius to 70, or stroke width to 2rem, or circle size to 200? I think probably not, and now that's all in our control - I've left curly braces wherever I would continue to add dynamic values. At the moment the component takes just percentage and colour, but it could take stroke width, radius, rounded ends and so on.

You can see the final code with some examples where I've added some more colours, rounded the ends using stroke-linecap="round" below; I've also included a "Randomise" button so you can see it in action.

Top comments (4)

Collapse
 
netanelben profile image
Netanel Ben

Awesome work mate! 💪

Collapse
 
tomaszwagner1 profile image
TomW

Awesome work mate! 💪

Collapse
 
muhghazaliakbar profile image
Muh Ghazali Akbar

Is there a way to make the progress counter clockwise?

Collapse
 
kiransiddi profile image
kiransiddi

how to this for semicircle , any idea