Designing a score meter with React, SVG, and CSS.
Tags: react, svg, css, webdev
Published: 2023-09-06 12:26:29

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rh9w597fiiy5lt8532ln.png)



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

{% codepen https://codepen.io/SupunKavinda/pen/xxmRJVr %}

## Parts of the Score Meter

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


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0skt3guytuwxnoir5974.png)

> 💡 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.

```tsx
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>
  );
}
```

### 2. Progress Bar SVG

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

```tsx
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>
  );
}
```

### 3. CSS

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

```css
.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;
}
```

#### 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:

```html
<svg width="200" height="120">
	<circle
        cx="100"
        cy="100"
        r="65"
  	></circle>
</svg>
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vmkzkbh49g6804xr56an.png)

Then, remove the fill and add a stroke.

```css
<svg width="200" height="120">
	<circle
        cx="100"
        cy="100"
        r="65"
        fill="none"
        stroke-width="25"
        stroke="#e5e5e5"
  	></circle>
</svg>
```


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w13mrjmm8pfkbmlq7rkg.png)

Then, most of the magic is done using [stroke-dasharray](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray) and [stroke-dashoffset](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k5ba7a8m92ify1secrf2.png)

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


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xajd5ayyl466efvf0nzr.png)


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

```tsx
const dashArray = (Math.PI * radius * widthPerc) / 100;
// ...
strokeDasharray={`${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,

```txt
Math.PI * radius
= 3.14 * 65
= 204.2
```

When we add it to our circle:

```html
<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>
```

we get this:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2wbv0l29pylchyt4zc31.png)

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,

```txt
-1 * Math.PI * radius
 = -3.14 * 65
 = -204.2
```

So, our final SVG code looks like this:

```html
<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>
```

we get what we need!


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yl4ae3030ws5rh8p7vwf.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qunnobdvvkzkkobcjfbh.png)


Check out [Hyvor Blogs](https://blogs.hyvor.com/) and all the features it provides to make blogging super easy.

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