Wheel of Fortune with CSS

Mads Stoumann - May 23 - - Dev Community

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

The markup

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

<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>
Enter fullscreen mode Exit fullscreen mode

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

: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;
}
Enter fullscreen mode Exit fullscreen mode

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

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

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;
}
Enter fullscreen mode Exit fullscreen mode

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

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:

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. */
}
Enter fullscreen mode Exit fullscreen mode

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

li {
  rotate: calc(360deg / var(--_items) * calc(var(--_idx) - 1));
}
Enter fullscreen mode Exit fullscreen mode

With rotate

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

li {
  background: hsl(calc(360deg / var(--_items) *
  calc(var(--_idx))), 100%, 75%);
}
Enter fullscreen mode Exit fullscreen mode

Color Variations


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
Enter fullscreen mode Exit fullscreen mode

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

li {
  height: calc((2 * pi * 50cqi) / var(--_items));
}
Enter fullscreen mode Exit fullscreen mode

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

With pi for height

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:

li {
  clip-path: polygon(0% 0%, 100% 50%, 0% 100%);
}
Enter fullscreen mode Exit fullscreen mode

With clip-path

Let's deduct a little from the edges:

li {
  clip-path: polygon(0% -2%, 100% 50%, 0% 102%);
}
Enter fullscreen mode Exit fullscreen mode

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

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

.wheel-of-fortune {
  clip-path: inset(0 0 0 0 round 50%);
}
Enter fullscreen mode Exit fullscreen mode

Much better:

Wheel Of Fortune, Final

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:

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;
  });
}
Enter fullscreen mode Exit fullscreen mode

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!

Full code is here:

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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player