DEV Community

Cover image for How to create perfect CSS circle sectors
Andrei Gheorghiu
Andrei Gheorghiu

Posted on

How to create perfect CSS circle sectors

Spinning the wheel: building a jackpot roulette

As a CSS enthusiast, you’ve probably dabbled in gradients, animations, and maybe even a bit of trigonometry. But have you ever built a responsive, interactive CSS jackpot roulette? Recently, I had to tackle just that, and it turned out to be an challenging exercise in geometry, creativity, and a sprinkle of frontend magic. Let me take you through the process step by step.


The challenge: responsive highlightable sectors

The roulette needed to:

  1. Highlight the sectors as the needle pointed at them.
  2. Be fully responsive, adapting to screen size.
  3. Allow a variable number of sectors—a critical feature that ruled out using static images or SVGs.

This meant one thing: we’d need to draw the sectors dynamically and calculate their shapes and positions geometrically. Intrigued? Let’s dive in.


The setup: rotating spans around the center

To start, I used <span> elements to represent the sectors, rotating each around the center of the circle. Each sector would be clipped along its radius to avoid overlap. You can take a peek at my initial setup here.

For the rotation, I iterated through the sectors, incrementally rotating them by:

transform: rotate(calc(360deg / number_of_sectors * index));
Enter fullscreen mode Exit fullscreen mode

At this stage, everything was pretty simple. I had a circle, some rotated spans of text and rays in between them. But in order to highlight a sector I needed to draw its precise shape.


The math: highlighting a sector

The first thing I needed to calculate was the sector height based on the angle between its boundaries and the radius. The formula for the distance between two points on a circle, given the radius and angle, is:

2 * radius * Math.sin(θ / 2)
Enter fullscreen mode Exit fullscreen mode

Where θ is the angle in radians. For our evenly spaced sectors, θ becomes:

2 * Math.PI / number_of_sectors
Enter fullscreen mode Exit fullscreen mode

Plugging that in:

2 * radius * Math.sin(Math.PI / number_of_sectors)
Enter fullscreen mode Exit fullscreen mode

This gave me the precise height of each sector (or, should I say, its dimension on the axis perpendicular to its text orientation).


The clipping: precision meets aesthetic

Initially, I tried clipping each sector from the top-right corner to the left-center and then to the bottom-right corner:

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

This worked… sort of. With many sectors, it was passable. But with fewer sectors—say three—it was, glaringly wrong:

Sectors clipped wrong

The fix? Calculating the exact intersection points between the sector and the circle, clipping from there to the center. After some research, I found the formulas for the segments formed on a horizontal ray by the line connecting the two points on the circle:

Image for segment formulas

  • Segment close to the center:
const m = radius * (1 - Math.cos(θ / 2))
Enter fullscreen mode Exit fullscreen mode
  • Segment farther from the center:
const n = radius * Math.cos(θ / 2)
Enter fullscreen mode Exit fullscreen mode

Just like above, θ is in radians:

2 * Math.PI / number_of_sectors
Enter fullscreen mode Exit fullscreen mode

The ratio between the two segments gave me the exact clipping point:

const clipPosition = 100 * (m + n) / m
Enter fullscreen mode Exit fullscreen mode

Plugging in to our example:

const clipPosition = Math.cos(Math.PI / number_of_sectors) * 100
Enter fullscreen mode Exit fullscreen mode

...which led to the final clip-path:

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

Now the sectors aligned perfectly:
Sectors clipped correctly


Bringing it to life

I implemented the spinning logic using Vue. Clicking the center produces a random spin, while clicking on a sector spins the roulette a few times and then lands on that sector. You can check out the result here.
Code available here.


Closing thoughts

Hope you enjoyed this exploration. What’s the next CSS challenge you’re tackling? Let me know — I’m always eager to swap tricks and ideas!

Top comments (0)