← blog
CSSAugust 17, 2026 · 4 min read

Stop Writing Math.cos() in a Resize Listener

CSS learned trigonometry. Here's the radial menu that used to need a resize listener — and doesn't anymore.

Parsa Jiravand · Frontend engineer · building bestpractic
Stop Writing Math.cos() in a Resize Listener

You've written this function before, maybe without thinking twice about it:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function layoutRadial(container, radius) { const items = [...container.children]; const step = (2 * Math.PI) / items.length; items.forEach((item, i) => { const angle = i * step; const x = Math.cos(angle) * radius; const y = Math.sin(angle) * radius; item.style.transform = `translate(${x}px, ${y}px)`; }); } layoutRadial(menu, 120); window.addEventListener("resize", () => layoutRadial(menu, getRadius()));

A radial nav, a rating dial, a pie-chart label ring, a "share" burst menu — anything arranged in a circle gets this same loop. It works. It also means your layout now lives in a resize handler, re-running trig on the main thread every time the viewport twitches.

Here's the thing: you don't need the loop anymore. CSS does the trig now.

"CSS can't do math beyond calc() plus and minus" was true for a long time, and a decade of Stack Overflow answers, boilerplate, and muscle memory got built on top of that fact. If you wanted a circle of evenly spaced elements, JavaScript was the only tool with sin and cos in it, so JavaScript computed the positions — and JavaScript re-computed them on every resize, because inline pixel values don't respond to anything on their own.

That belief is why the layoutRadial function above still gets written today, copy-pasted into new components, in 2026.

The CSS Values and Units spec added trigonometric functions — sin(), cos(), tan(), asin(), acos(), atan(), atan2() — and they've been in every major engine (Chrome, Firefox, Safari) since early 2023. They work inside calc()-style math anywhere a number or length is expected, and they take custom properties as input just like anything else in CSS.

That means the radial math itself — turn an index into an angle, turn an angle into an x/y offset — can live in a stylesheet:

CSS
1
2
3
4
5
6
7
8
9
10
11
12
.radial-item { --angle: calc(360deg / var(--count) * var(--i)); position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) translate( calc(cos(var(--angle)) * var(--radius)), calc(sin(var(--angle)) * var(--radius)) ); }

cos() and sin() return a plain number, and CSS happily multiplies that number by a length (var(--radius)) to get another length. No JavaScript touches a pixel value here — the browser's own math engine does the trig every time it lays out the page, including on resize, for free.

JavaScript still shows up, but only once, to give each item its index and the group its count:

JavaScript
1
2
3
const items = [...menu.children]; items.forEach((item, i) => item.style.setProperty("--i", i)); menu.style.setProperty("--count", items.length);

That's it. No loop over Math.cos. No resize listener. Set --radius to a clamp() with container query units — clamp(60px, 22cqi, 140px) — and the circle resizes itself when its container does, with zero JavaScript involved in the resize at all.

Runs right in your browser — poke at it and watch the concept react live.

Try it yourself before you take my word for the "no resize listener" part — drag the radius and item-count sliders in the playground above and watch the circle stay perfectly spaced with nothing but CSS custom properties changing.

This isn't "CSS replaced JavaScript for graphics." atan2() in CSS is genuinely useful for static angle math — pointing an arrow at a fixed target, say — but CSS still has no way to read where the user's mouse is. A draggable dial that needs to convert pointer position into an angle still needs Math.atan2(dy, dx) in a pointermove handler, because that math depends on live input CSS can't see.

The honest split: static or state-driven circular layout — anything you can express as "N items, spaced evenly, at some radius" — moves to CSS entirely. Reading a live pointer angle to drive that state stays JavaScript's job. You still write Math.atan2, you just write it to compute one number (the value the user dragged to), not to reposition every element in the layout by hand.

If you're placing things in a circle, JavaScript's job is handing CSS an index and a count — not doing the trigonometry. The browser has had sin(), cos(), and atan2() in its stylesheet engine since 2023; if your radial layout still has a resize listener recomputing Math.cos, that's not necessary anymore, it's just what the code looked like before the platform caught up.

What's the oldest "JS does the math, CSS just applies it" pattern still sitting in your codebase? I'd bet trig isn't the only one.

Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.


Thanks for reading! Let's stay connected:

Keep reading

One post a day, in your inbox

Each one with a runnable playground and a quiz. No pitch, no digest, unsubscribe in one click.

Originally published on dev.to