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:

Score Meter
Final Result

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

See the Pen Untitled by Supun Kavinda (@SupunKavinda) on CodePen.

Parts of the Score Meter

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

Parts of the Score Meter
Parts of the Score Meter

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.

1export default function App({ score } : { score: number }) {
2
3 return (
4 <div className="score-wrap">
5 <div className="score">
6 <div className="score-bar">
7 <div className="placeholder">{progressBar(100)}</div>
8 <div className="score-circle">{progressBar(score, true)}</div>
9 </div>
10 <div className="score-value">
11 <div className="score-name">Score</div>
12 <div className="score-number">
13 {Math.round(score)}%
14 </div>
15 </div>
16 </div>
17 </div>
18 );
19}

2. Progress Bar SVG

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

1function progressBar(widthPerc: number, gradient: boolean = false) {
2 const radius = 65;
3 const dashArray = (Math.PI * radius * widthPerc) / 100;
4
5 return (
6 <svg width="200" height="120">
7 <circle
8 cx="100"
9 cy="100"
10 r={radius}
11 fill="none"
12 strokeWidth="25"
13 strokeLinecap="round"
14 strokeDashoffset={-1 * Math.PI * radius}
15 strokeDasharray={`${dashArray} 10000`}
16 stroke={gradient ? "url(#score-gradient)" : "#e5e5e5"}
17 ></circle>
18 {gradient && (
19 <defs>
20 <linearGradient id="score-gradient">
21 <stop offset="0%" stopColor="red" />
22 <stop offset="25%" stopColor="orange" />
23 <stop offset="100%" stopColor="green" />
24 </linearGradient>
25 </defs>
26 )}
27 </svg>
28 );
29}

3. CSS

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

1.score-wrap {
2 display: flex;
3 justify-content: center;
4 margin-bottom: 20px;
5}
6.score {
7 width: 200px;
8 height: 120px;
9 position: relative;
10 overflow: hidden;
11 display: flex;
12 align-items: flex-end;
13 justify-content: center;
14}
15
16.score-bar {
17 position: absolute;
18 width: 100%;
19 height: 200%;
20 border-radius: 50%;
21 top: 0;
22}
23.score-bar .score-circle {
24 position: absolute;
25 top: 0;
26}
27
28.score-value {
29 margin-bottom: 5px;
30 text-align: center;
31}
32.score-name {
33 color: #777;
34}
35.score-number {
36 font-size: 25px;
37 font-weight: 600;
38}
39

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:

1<svg width="200" height="120">
2 <circle
3 cx="100"
4 cy="100"
5 r="65"
6 ></circle>
7</svg>
progress bar svg with circle

Then, remove the fill and add a stroke.

1<svg width="200" height="120">
2 <circle
3 cx="100"
4 cy="100"
5 r="65"
6 fill="none"
7 stroke-width="25"
8 stroke="#e5e5e5"
9 ></circle>
10</svg>
progress bar svg with stroke

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.

progress bar svg with stroke dasharray

Then, adding stroke-linecap=ā€roundā€ gives you nice rounded dashes.

progress bar svg with stroke linecap round

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

1const dashArray = (Math.PI * radius * widthPerc) / 100;
2// ...
3strokeDasharray={`${dashArray} 10000`}

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,

1Math.PI * radius
2= 3.14 * 65
3= 204.2

When we add it to our circle:

1<svg width="200" height="120">
2 <circle
3 cx="100"
4 cy="100"
5 r="65"
6 fill="none"
7 stroke-width="25"
8 stroke="#e5e5e5"
9 stroke-dasharray="204.2 10000"
10 stroke-linecap="round"
11 ></circle>
12</svg>

we get this:

progress bar svg with stroke dasharray

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-1 * Math.PI * radius
2 = -3.14 * 65
3 = -204.2

So, our final SVG code looks like this:

1<svg width="200" height="120">
2 <circle
3 cx="100"
4 cy="100"
5 r="65"
6 fill="none"
7 stroke-width="25"
8 stroke="#e5e5e5"
9 stroke-dasharray="204.2 10000"
10 stroke-linecap="round"
11 stroke-dashoffset="-204.2"
12 ></circle>
13</svg>

we get what we need!

Progressbar SVG completed

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:

SEO Analyzer Hyvor Blogs

Check out Hyvor Blogs and all the features that make blogging super easy.

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