Trigonometry lands in CSS

CSS has supported mathematical expressions for a while now, starting with calc() and later joined by min(), max(), and clamp(). The spec goes further: the CSS Values and Units Module Level 4 also defines trigonometric functions, and all major browsers now ship support for them.

You get the full set: sin(), cos(), tan() plus their inverse counterparts asin(), acos(), atan(), and the two-argument atan2().

The core functions: sin(), cos(), tan()

These three take an angle as their argument and return:

  • cos(): cosine of the angle, a value between -1 and 1
  • sin(): sine of the angle, a value between -1 and 1
  • tan(): tangent of the angle, ranging from −∞ to +∞

Unlike JavaScript's Math.sin() and friends, these CSS functions accept both angles and radians as input.

A typical use case is drawing the sides of a triangle around a circle. Given a custom property --angle and a --radius, you can position the sides geometrically: the adjacent side equals --radius multiplied by cos(--angle), and the opposite side uses --radius times sin(--angle). The tangent of the angle produces the line shooting from the dot toward the X-axis.

Inverse functions: going from numbers back to angles

The arc functions reverse the direction of the calculation. Instead of taking an angle and returning a ratio, they take a numeric value and return the angle that corresponds to it:

  • asin(): inverse of sine
  • acos(): inverse of cosine
  • atan(): inverse of tangent
  • atan2(): takes two arguments A and B, returns the angle between the positive X-axis and the point (B,A)

Orbiting elements around a center

One immediate use case is arranging dots along a circular path without rotating the container. Instead of rotating each dot about its own center and pushing it outward, you translate it along the X and Y axes. The translation distance comes from cos() and sin() applied to a shared --angle custom property:

:root {
  --radius: 20vmin;
}

.dot {
  --angle: 30deg;
  translate: /* Translation on X-axis */
             calc(cos(var(--angle)) * var(--radius))

             /* Translation on Y-axis */
             calc(sin(var(--angle)) * var(--radius) * -1)
  ;
}

To space the dots evenly, each dot receives an offset computed from its position in the markup. With three dots, the gap is 120deg (i.e., 360deg / 3). The first child gets 0 x 120deg = 0deg, the second gets 1 x 120deg = 120deg, and the third gets 2 x 120deg = 240deg.

Facing a target with atan2()

To rotate an element so it points at another element or at the mouse cursor, you need the angle between them. The atan2() function provides it. Given the target's y and x offset relative to the origin point at 0,0, the function returns the angle to rotate the element to face that location.

A common pattern is syncing the mouse position to CSS custom properties via JavaScript, then using them in a transform:

div.box {
  --my-x: 200;
  --my-y: 300;

  /* Position the box inside its parent */
  position: absolute;
  width: 50px;
  aspect-ratio: 1;
  translate: calc((var(--my-x) * 1px)) calc(var(--my-y) * 1px);

  /* Rotate so that the box faces the mouse position */
  /* For this, take the box its own position and size (25 = half the width) into account */
  rotate: atan2(
            calc((var(--mouse-x) - var(--my-x) - 25) * 1),
            calc((var(--mouse-y) - var(--my-y) - 25) * -1)
          );
}

Beyond layout

The functions are not restricted to positioning. In Ana Tudor's animated Möbius strip demo, the output of cos() and sin() is used to drive the saturation and lightness components of the hsl() color function, producing smooth, continuously shifting colors along the surface.