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:
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>
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;
}
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;
}
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:
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. */
}
With that we only need to add one more line of CSS:
li {
rotate: calc(360deg / var(--_items) * calc(var(--_idx) - 1));
}
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%);
}
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:
li {
height: calc((2 * pi * 50cqi) / var(--_items));
}
Isn't it just cool that we have pi in CSS now?!
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%);
}
Let's deduct a little from the edges:
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:
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%);
}
Much better:
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;
});
}
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
andaspect-ratio
(see comments below).
More ideas
I encourage you to play around with other styles! Maybe add a dotted border?
Top comments (14)
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 ratioaspect-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
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.
Has anyone solved the return value of the winning field? For example index for li tag. I have something, but the results are +- one position.
I think it’s resolved ( Link on Codepen.io ).
Cool!
Please share — I need to think about it too!
wholesome wheel! :)
Beautiful project!
Thank you!
Always here for an interesting CSS project. Kudos.
Thanks!
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?
I don't think that'd be easier in javascript. Whenever you have something visual, the right tool for that is CSS. When it's easier in pure JS, this usually mean that you lack knowledge with CSS.
Oh, have fun to rebuild the [solarsystem][dev.to/cookiemonsterdev/solar-syst...] with pure CSS. There are good reasons the S in
CSS comes from "style", not from graphics...