DEV Community

Cover image for Designing a score meter with React, SVG, and CSS.
Supun Kavinda
Supun Kavinda

Posted on • Originally published at supun.io

Designing a score meter with React, SVG, and CSS.

I recently worked on creating an SEO analyzer for Hyvor Blogs, which required a score meter to display the SEO score. This is a short guide on how I designed it. The final result will look like this:

Image description

We’ll be using React, but you can easily use the same concept with any other framework. The ‘react part’ is quite trivial.

Full Code on Codepen

Parts of the Score Meter

These are the parts of the score meter. We’ll be using SVG for the progress bar.

Image description

💡 As someone who is not too familiar with SVG, my first thought was to design the full component using just CSS. I tried several different ways using circles, etc. However, I could not get the perfect round edges. So, here comes SVG!

Building the Score Meter

1. Main Component

This is what the main structure would look like. It takes one prop: score, which is a number between 0 to 100.

export default function App({ score } : { score: number }) {

  return (
    <div className="score-wrap">
        <div className="score">
            <div className="score-bar">
                <div className="placeholder">{progressBar(100)}</div>
                <div className="score-circle">{progressBar(score, true)}</div>
            </div>
            <div className="score-value">
                <div className="score-name">Score</div>
                <div className="score-number">
                    {Math.round(score)}%
                </div>
            </div>
        </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

2. Progress Bar SVG

Now, let’s design the progress bar component, the svg used for both the placeholder and the fill.

function progressBar(widthPerc: number, gradient: boolean = false) {
  const radius = 65;
  const dashArray = (Math.PI * radius * widthPerc) / 100;

  return (
    <svg width="200" height="120">
      <circle
        cx="100"
        cy="100"
        r={radius}
        fill="none"
        strokeWidth="25"
        strokeLinecap="round"
        strokeDashoffset={-1 * Math.PI * radius}
        strokeDasharray={`${dashArray} 10000`}
        stroke={gradient ? "url(#score-gradient)" : "#e5e5e5"}
      ></circle>
      {gradient && (
        <defs>
          <linearGradient id="score-gradient">
            <stop offset="0%" stopColor="red" />
            <stop offset="25%" stopColor="orange" />
            <stop offset="100%" stopColor="green" />
          </linearGradient>
        </defs>
      )}
    </svg>
  );
}
Enter fullscreen mode Exit fullscreen mode

3. CSS

Add some CSS to position the text and the progress bars correctly.

.score-wrap {
  display: flex;
  justify-content: center;
  margin-bottom: 20px;
}
.score {
  width: 200px;
  height: 120px;
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

.score-bar {
  position: absolute;
  width: 100%;
  height: 200%;
  border-radius: 50%;
  top: 0;
}
.score-bar .score-circle {
  position: absolute;
  top: 0;
}

.score-value {
  margin-bottom: 5px;
  text-align: center;
}
.score-name {
  color: #777;
}
.score-number {
  font-size: 25px;
  font-weight: 600;
}
Enter fullscreen mode Exit fullscreen mode

Progress Bar SVG Explanation

I think the main component and CSS parts are self-explanatory. I’ll explain how the SVG works step-by-step.

First, add a circle in an SVG canvas:

<svg width="200" height="120">
    <circle
        cx="100"
        cy="100"
        r="65"
    ></circle>
</svg>
Enter fullscreen mode Exit fullscreen mode

Image description

Then, remove the fill and add a stroke.

<svg width="200" height="120">
    <circle
        cx="100"
        cy="100"
        r="65"
        fill="none"
        stroke-width="25"
        stroke="#e5e5e5"
    ></circle>
</svg>
Enter fullscreen mode Exit fullscreen mode

Image description

Then, most of the magic is done using stroke-dasharray and stroke-dashoffset.

stroke-dasharray allows you to define a pattern for dashes and gaps. For example, if you add stroke-dasharray="20", you will see something like this.

Image description

Then, adding stroke-linecap="round" gives you nice rounded dashes.

Image description

Let's do some calculations to set the stroke-dasharray property correctly. In our React code, we use the following:

const dashArray = (Math.PI * radius * widthPerc) / 100;
// ...
strokeDasharray={`${dashArray} 10000`}
Enter fullscreen mode Exit fullscreen mode

Math.PI * radius is equal to half of the circumference of the circle. That's what we need for the placeholder of the progress bar (fill depends on the current score).

Note: the 10000 in strokeDasharray is the gap. Because, we only need one dash, I used 10000 to add a large gap to make sure only one dash is drawn. You can set it to anything larger than πr.

So,

Math.PI * radius
= 3.14 * 65
= 204.2
Enter fullscreen mode Exit fullscreen mode

When we add it to our circle:

<svg width="200" height="120">
    <circle
        cx="100"
        cy="100"
        r="65"
        fill="none"
        stroke-width="25"
        stroke="#e5e5e5"
        stroke-dasharray="204.2 10000"
        stroke-linecap="round"
    ></circle>
</svg>
Enter fullscreen mode Exit fullscreen mode

we get this:

Image description

Finally, let's use stroke-dashoffset to change the position where the first dash is drawn. In our react code, we use strokeDashoffset={-1 * Math.PI * radius}. So,

-1 * Math.PI * radius
 = -3.14 * 65
 = -204.2
Enter fullscreen mode Exit fullscreen mode

So, our final SVG code looks like this:

<svg width="200" height="120">
    <circle
        cx="100"
        cy="100"
        r="65"
        fill="none"
        stroke-width="25"
        stroke="#e5e5e5"
        stroke-dasharray="204.2 10000"
        stroke-linecap="round"
        stroke-dashoffset="-204.2"
    ></circle>
</svg>
Enter fullscreen mode Exit fullscreen mode

we get what we need!

Image description

This is the placeholder of the progress bar. In the fill, we use a gradient. Both are positioned on top of each other using CSS.

Final thoughts

While the score meter UI seems simple, creating it correctly takes some effort, especially if you are not very familiar with SVG. I hope this article helped you learn about SVG circles, dashes, and gaps. Feel free to reuse the code in your projects.

As mentioned earlier, I wrote this score meter for the SEO analyzer of Hyvor Blogs, which analyzes blog posts as you write and gives you feedback in real time. Here’s what it looks like for this post:

Image description

Check out Hyvor Blogs and all the features it provides to make blogging super easy.

If you have any feedback on the article, feel free to comment below.

Top comments (0)