DEV Community

Cover image for Stop Writing Math.cos() in a Resize Listener
Parsa Jiravand
Parsa Jiravand

Posted on • Originally published at bestpractic.org

Stop Writing Math.cos() in a Resize Listener

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

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

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.

The belief that made sense until it didn't

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

What actually shipped

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:

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

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:

const items = [...menu.children];
items.forEach((item, i) => item.style.setProperty("--i", i));
menu.style.setProperty("--count", items.length);
Enter fullscreen mode Exit fullscreen mode

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.

🎮 Try it yourself

▶️ Open the interactive playground →

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.

Where JS still earns its keep

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.

The one-line version

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.

🧠 Test yourself

Think it clicked? Take the 8-question quiz →

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


Thanks for reading! Let's stay connected:

Top comments (0)