Creating an interactive SVG: The circle of fifths
Tags: svg, css, webdev
Published: 2023-02-09 17:20:55

**Written by [Mads Stoumann](https://blog.logrocket.com/author/madsstoumann/)✏️**


Not so long ago, these beautiful posters showed up in an advertisement on one of my social feeds: ![Circle Of Fifths Ad](https://blog.logrocket.com/wp-content/uploads/2023/01/circle-of-fifths-ad.jpeg) 

I typically ignore all ads, but these appealed to me right away — maybe because I used to study music in high school and play in a band, or maybe because I used to work in graphic design, and these are just beautiful! 

Right away, I wanted to recreate them in `<svg>`. I often recreate art in `<svg>` as an exercise to improve my skills. But then it struck me: What if I could make the posters come alive by making them interactive, flexible, and responsive? 

In this tutorial, we’ll be recreating the posters above using SVG, CSS, and a bit of math. 

To jump ahead:

*   [Arcs and circles](#arcs-circles)
*   [Placing elements around a circle](#placing-elements-around-circle)
*   [Styling the circle with `fill`s and `stroke`s](#styling-circle-fills-strokes)
*   [Customizing our circle’s aesthetic](#customizing-circles-aesthetic)

## Arcs and circles <a name="arcs-circles">

In music theory (and in the words of Wikipedia), the [circle of fifths](https://en.wikipedia.org/wiki/Circle_of_fifths) is a way of organizing the 12 chromatic pitches as a sequence of perfect fifths. 

The circle has three rings. The outer ring contains the staff with either flats (b) or sharps (#). The middle ring contains the major chords, and the inner ring contains the minor chords. 

A full circle is 360 degrees, so each “chromatic pitch” will be 30 (360/12) degrees. 

Norwegian developer Håken Lid has [developed some useful JavaScript functions](https://observablehq.com/@haakenlid/svg-circle) for creating `<svg>` circle segments. For our purposes, we will be using the `polarToCartesian` and `segmentPath` methods:

```css
function polarToCartesian(x, y, r, degrees) {
  const radians = degrees * Math.PI / 180.0;
  return [x + (r * Math.cos(radians)), y + (r * Math.sin(radians))]
}
function segmentPath(x, y, r0, r1, d0, d1) {
  const arc = Math.abs(d0 - d1) > 180 ? 1 : 0
  const point = (radius, degree) =>
    polarToCartesian(x, y, radius, degree)
      .map(n => n.toPrecision(5))
      .join(',')
  return [
    `M${point(r0, d0)}`,
    `A${r0},${r0},0,${arc},1,${point(r0, d1)}`,
    `L${point(r1, d1)}`,
    `A${r1},${r1},0,${arc},0,${point(r1, d0)}`,
    'Z',
  ].join('')
} 
```

The first method is used to convert polar coordinates into Cartesian coordinates, and the second is to create paths with arcs in SVG. 

Next, we’ll create our own `segment` method that will call Håken’s `segmentPath` method for each “chunk”:

```css
function segment(index, segments, size, radius, width) {
  const center = size / 2
  const degrees = 360 / segments 
  const start = degrees * index 
  const end = (degrees * (index + 1) + 1)
  const path = segmentPath(center, center, radius, radius-width, start, end)
  return `<path d="${path}" />`
}
```

`index` is the current “segment” (one of 12, in our case) and `segments` represents the total amount of segments (again, 12 in our case). `size` is the diameter of the circle (and the `viewBox` of our SVG). `radius` is normally half the diameter, but because we need three “rings,” we need to be able to change it for each “ring.” Finally, `width` is the height of the arc. 

Let’s call this method 12 times, using a loop, updating `index` for each iteration:

```css
segment(index, 12, size = 300, radius = 150, width = 150)
```

If `width` is set to the same value as `radius`, the arc will fill out the circle: ![The Arc Filling The Circle](https://blog.logrocket.com/wp-content/uploads/2023/01/filling-circle-arc.png) 

However, if we change `width` to 50, it will only fill up one third of the circle (because 50 is one third of 150): ![Changing The Inside Width Of The Circle](https://blog.logrocket.com/wp-content/uploads/2023/01/changing-circle-inside-width.png) 

Let’s add the other circles by calling our `segment` method multiple times within our loop:

```css
segment(index, 12, 300, 100, 30) /* radius = 100, width = 30)
segment(index, 12, 300, 70, 30) /* radius = 70, width = 30)
```

Now we have this — which almost looks like Spider-Man 😁: ![Adding More Circles Within The Larger Circle](https://blog.logrocket.com/wp-content/uploads/2023/01/adding-more-circle-inside.png) 

In our circle of fifths, the text should be placed exactly on the lines as we see above. However, the arcs themselves should not. 

Let’s use a CSS `transform` function to rotate the arcs. As each “chunk” is 30 degrees, we need to rotate them half of that — 15 degrees:

```css
transform: rotate(-15deg);
transform-origin: 50% 50%;
```

This gives us: ![Rotating The Arcs Within The Circle](https://blog.logrocket.com/wp-content/uploads/2023/01/rotating-arcs-circle.png) 

Getting closer! 

Now, let’s add the staff, flats, and sharps. I grabbed the elements I needed from [Wikimedia Commons](https://commons.wikimedia.org/wiki/Category:SVG_musical_notation), cleaned them up with Jake Archibald’s [SVGOMG](https://jakearchibald.github.io/svgomg/), and then I converted each into a `<symbol>` so I can `<use>` them multiple times. 

But before we add these elements and fill out our circles, we should organize our data. Let’s create an array of 12 objects, containing the labels and amount of flats or sharps:

```css
{
  outer: {
    amount: 4,
    use: 'flat'
  },
  middle: {
    label: 'A<tspan baseline-shift="super">b</tspan>'
  },
  inner: {
    label: 'Fm'
  }
}, /* etc */
```

What’s up with `<tspan baseline-shift="super">`? Because we’re in `<svg>` land, we can’t use `<sup>`. So for chords like A flat, we replace `<sup>` with `baseline-shift`.

## Placing elements around a circle <a name="placing-elements-around-circle">

To place an element in a circle, we need the center point of the circle, the radius of the circle, the angle, and some math:

```css
function posXY(center, radius, angle) {
  return [
    center + radius * Math.cos(angle * Math.PI / 180.0), 
    center + radius * Math.sin(angle * Math.PI / 180.0)
  ]
}
```

Now let’s combine all the examples into one big “render” chunk. `data` is the array of objects we created earlier:

```css
const size = 300; /* diameter / viewBox */
const radius = size/2;
const svg =  data.map((obj, index) => {
  const angle = index * (360 / data.length);    
  const [x0, y0] = posXY(radius, 125, angle);
  const [x1, y1] = posXY(radius, 85, angle);
  const [x2, y2] = posXY(radius, 55, angle);
  return `
  <g class="cf-arcs">
    ${segment(index, data.length, size, radius, 0, 50)}
    ${segment(index, data.length, size, 100, 0, 30, obj.middle.notes)}
    ${segment(index, data.length, size, 70, 0, 30, obj.inner.notes)}
  </g>
  <g transform="translate(${x0-15}, ${y0-radius})">
    <use width="30" xlink:href="#staff"></use>
    ${Array.from(Array(obj.outer.amount).keys()).map(i => `
      <use width="2" xlink:href="#${obj.outer.use}" class="cf-${obj.outer.use}-${i+1}"></use>`
    ).join('')}
  </g>
  <text x="${x1}" y="${y1+3}" class="cf-text-middle">${obj.middle.label}</text>
  <text x="${x2}" y="${y2+2}" class="cf-text-inner">${obj.inner.label}</text>
  `
  }).join('')
```

## Styling the circle with `fill`s and `stroke`s <a name="styling-circle-fills-strokes">

It’s trivial to set the `background-color` of the page and place the circle centrally. The most important parts are the `<path>`s we created earlier. Without `fill` or `stroke`, our circle looks like this: ![Styling The Circle With Fills And Strokes](https://blog.logrocket.com/wp-content/uploads/2023/01/styling-circle-fills-strokes.png) Let’s add some simple styling, with a stroke that matches the `background-color`:

```css
path {
  fill: hsl(348, 60%, 10%);
  stroke: hsl(348, 60%, 52%);
}
```

… and while we’re at it, why not add a `hover` effect:

```css
path:hover {
  fill: hsl(348, 60%, 25%);
}
```

We finally have our circle of fifths! Isn’t it beautiful?

See the Pen [Circle of Fifths](https://codepen.io/stoumann/pen/abLeMVM) by Mads Stoumann ([@stoumann](https://codepen.io/stoumann)) on [CodePen](https://codepen.io).

## Customizing our circle’s aesthetic <a name="customizing-circles-aesthetic">

Now, let’s create the dusty blue version — and while we’re at it, let’s add some subtle noise to make it look vintage:

See the Pen [Circle of Fifths – Blue Vintage](https://codepen.io/stoumann/pen/qBPevxe) by Mads Stoumann ([@stoumann](https://codepen.io/stoumann)) on [CodePen](https://codepen.io).

The grainy, noise filter is an [SVG](https://blog.logrocket.com/complete-guide-using-css-filters-svgs/) [filter](https://blog.logrocket.com/complete-guide-using-css-filters-svgs/), used as a CSS background.

## Conclusion

In this tutorial, we learned how to code SVG from scratch, using a bit of math. Placing data in a circle does not have to be difficult, and with [trigonometric funct](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#trigonometric_functions)[i](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#trigonometric_functions)[ons](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#trigonometric_functions) coming soon to CSS, it’s going to be even easier. 

I hope you had fun reading — hopefully this article inspired you to do some creative coding with SVG! Check out these other articles if you are interested in using [CSS filters with SVGs](https://blog.logrocket.com/complete-guide-using-css-filters-svgs/) or [animating SVGs with CSS](https://blog.logrocket.com/how-to-animate-svg-css-tutorial-examples/). 

Here’s the [original poster](https://detkuloerteudvalg.dk/vare/det-kuloerte-udvalg-kvintcirklen-plakat/) that inspired this tutorial.

---

## Is your frontend hogging your users' CPU?

As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, [try LogRocket] (https://logrocket.com/signup/).

[![LogRocket Dashboard Free Trial Banner] (https://blog.logrocket.com/wp-content/uploads/2019/12/cpu-memory-usage.png)](https://logrocket.com/signup/)

[LogRocket] (https://logrocket.com/signup/) is like a DVR for web apps, recording everything that happens in your web app or site. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.

Modernize how you debug web apps — [Start monitoring for free] (https://logrocket.com/signup/).