DEV Community

Cover image for Wheel of Fortune with CSS
Mads Stoumann
Mads Stoumann

Posted on

Wheel of Fortune with CSS

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


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:

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

Top comments (7)

afif profile image
Temani Afif

The height calculation is actually not correct. You need to consider the polygon shape around the circle to find the correct height (the circumscribed polygon)

It's equal to height: calc(2*50cqi*tan(180deg/var(--_items))); that you can simplify by setting the ratio aspect-ratio: 1/calc(2*tan(180deg/var(--_items))); to avoid using the width value twice.

With this you won't have issue when you apply the clip-path

madsstoumann profile image
Mads Stoumann • Edited

AH, perfect — thank you! You truly are the master of CSS Shapes. I've added an update, and will use your input for the follow-up article with spinning.

jocomvag profile image
Jocom Vag

Always here for an interesting CSS project. Kudos.

madsstoumann profile image
Mads Stoumann


ddebajyati profile image
Debajyati Dey

wholesome wheel! :)
Beautiful project!

madsstoumann profile image
Mads Stoumann

Thank you!

efpage profile image

I suppose this would be easier done in pure Javasript using CSS only where It's appropriate (e.g. the animation). Is it really worth the effort doing anything in CSS?