Wheel of Fortune with CSS
Tags: css, webdev, tutorial, javascript
Published: 2024-05-23 08:21:31

A "Wheel of Fortune" component just popped up in my feed. I always spin, but never win! Anyway, this type of component is often built with `<canvas>`, so I thought I'd write a tutorial on how to make it in CSS. For the interactivity, you still have to use JavaScript.

Here's what we'll be building:

![Wheel Of Fortune](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8j69ov5s9gg37qm387zi.png)

## The markup
For the wedges, we'll be using a simple list:

```html
<ul class="wheel-of-fortune">
  <li>$1000</li>
  <li>$2000</li>
  <li>$3000</li>
  <li>$4000</li>
  <li>$5000</li>
  <li>$6000</li>
  <li>$7000</li>
  <li>$8000</li>
  <li>$9000</li>
  <li>$10000</li>
  <li>$11000</li>
  <li>$12000</li>
</ul>
```

OK, so we have a list of numbers. Now, let's set some initial styles:

```css
:where(.ui-wheel-of-fortune) {
  --_items: 12;
  all: unset;
  aspect-ratio: 1 / 1;
  background: crimson;
  container-type: inline-size;
  direction: ltr;
  display: grid;
  place-content: center start;
}
```

First is a variable we'll be using to control the amount of items. As the list has 12 items, we set `--_items: 12;`.

I set the `container-type` so we can use container-query units (more on that later), then a grid with content placed "left center". This gives us:

![Initial](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7r4q77mxq4muchg3abxr.png)

OK, doesn't look like much, let's look into the wedges:

```css
li {
  align-content: center;
  background: deepskyblue;
  display: grid;
  font-size: 5cqi;
  grid-area: 1 / -1;
  list-style: none;
  padding-left: 1ch;
  transform-origin: center right;
  width: 50cqi;
}
```

Instead of `position: absolute` we "stack" all the `<li>` in the same place in the grid using `grid-area: 1 / -1`. We set the `transform-origin` to `center right`, meaning we'll rotate the wedge around that axis.

So, now we have:

![With items](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bwdkpb86lnilr9rakmcl.png)

Because all the elements are stacked, we can only see the last.

Let's do something about that. First, we'll add an index variable to each wedge:

```css
li {
  &:nth-of-type(1) { --_idx: 1; }
  &:nth-of-type(2) { --_idx: 2; }
  &:nth-of-type(3) { --_idx: 3; }
  &:nth-of-type(4) { --_idx: 4; }
  &:nth-of-type(5) { --_idx: 5; }
  /* etc. */
}
```

With that we only need to add one more line of CSS:

```css
li {
  rotate: calc(360deg / var(--_items) * calc(var(--_idx) - 1));
}
```

![With rotate](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7631q4ogffnw7loeqlpj.png)

Getting there! Let's use the same variables to create some color variations:

```css
li {
  background: hsl(calc(360deg / var(--_items) *
  calc(var(--_idx))), 100%, 75%);
}
```

![Color Variations](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2lvak3kq1i5k6fpz4sdf.png)

---

## A Slice of π

For the **height** of the wedges we need the circumference of the circle divided by the amount of items. As you might recall from school, the circumference of a circle is:

```
C=2πr
```

Because we're using container-units, the **radius** is `50cqi`, so the formula we need in CSS is:

```css
li {
  height: calc((2 * pi * 50cqi) / var(--_items));
}
```

Isn't it just cool that we have **pi** in CSS now?!


![With pi for height](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/llxvvp7owtb2yyzd11d9.png)

Now, let's add a simple `clip-path` to each wedge. We'll start at the top left corner, move to the right center, then back to left bottom:

```css
li {
  clip-path: polygon(0% 0%, 100% 50%, 0% 100%);
}
```

![With clip-path](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n138btms1240z5w9ju6q.png)

Let's deduct a little from the edges:

```css
li {
  clip-path: polygon(0% -2%, 100% 50%, 0% 102%);
}
```

Not sure, if there's a mathematical _correct_ way to do this?

Anyway, now we just need to add `border-radius: 50%` to the wrapper:

![With border-radius](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8mk3jpct0s4nvu2knqhw.png)

Hmm, not good. Let's use a `clip-path` instead, with `inset` and `round`:

```css
.wheel-of-fortune {
  clip-path: inset(0 0 0 0 round 50%);
}
```

Much better:

![Wheel Of Fortune, Final](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q7kkqd5obp5eenyb83m0.png)

And because we used container-units for the wedges **and** the `font-size`, it's fully responsive!

---

## Make it spin

Now, let's add a spin-`<button>` (see CSS in code-example below) and trigger a spin using JavaScript:

```js
function wheelOfFortune(selector) {
  const node = document.querySelector(selector);
  if (!node) return;

  const spin = node.querySelector('button');
  const wheel = node.querySelector('ul');
  let animation;
  let previousEndDegree = 0;

  spin.addEventListener('click', () => {
    if (animation) {
      animation.cancel(); // Reset the animation if it already exists
    }

    const randomAdditionalDegrees = Math.random() * 360 + 1800;
    const newEndDegree = previousEndDegree + randomAdditionalDegrees;

    animation = wheel.animate([
      { transform: `rotate(${previousEndDegree}deg)` },
      { transform: `rotate(${newEndDegree}deg)` }
    ], {
      duration: 4000,
      direction: 'normal',
      easing: 'cubic-bezier(0.440, -0.205, 0.000, 1.130)',
      fill: 'forwards',
      iterations: 1
    });

    previousEndDegree = newEndDegree;
  });
}
```

Instead of adding and removing a css-class and updating a `@property` with a new rotation-angle, I opted for the simplest solution: [The Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API)!

Full code is here:

{% codepen https://codepen.io/stoumann/pen/yLWJPMr %}


> **UPDATE:** The shape-master, Temani Atif, has provided a much more elegant way to create the wedges using `tan` and `aspect-ratio` (see comments below).

---

## More ideas

I encourage you to play around with other styles! Maybe add a dotted border?

![Dotted border](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5xy8661vrb099q8oq0xb.png)