atan2()

Juan Diego Rodríguez on

The CSS atan2() trigonometric function takes two values, (YX), and returns the inverse of the tan() function. More specifically, it takes a point in space between -Infinity and Infinity and returns the angle between the positive X-axis and the point (XY), making sure it’s in the -180deg – 180deg range.

.element {
  --angle: atan2(var(--m-y), var(--m-x));
}

The atan2() function is defined in the CSS Values and Units Module Level 4 specification.

Syntax

atan2( <calc-sum>, <calc-sum>)

The atan2() function takes two comma-separated calculations that can resolve to either a number, a dimension (such as px or vw), or a percentage. Both calculations must resolve to the same type; otherwise, it makes the declaration invalid.

Note: Chrome doesn’t seem to accept percentage values at the time of writing.

Arguments

/* it can take numbers, dimensions and percentages, if kept consistent */
rotate: atan2(5, 10); /* ~26deg */
rotate: atan2(200px, 200px); /* 45deg */
rotate: atan2(-10%, 25%); /* -21deg */

/* it can take any number between -Infinity and Infinity */
rotate: atan2(Infinity, 0) /* 90deg */
rotate: atan2(Infinity, -Infinity) /* 135deg */

/* we can do inner calculations */
rotate: atan2(0.5, sqrt(3) / 2); /* 30deg */
rotate: atan2(-1 * sqrt(3) / 2, 0.5); /* -60deg */

Usually, when working with atan2(), we’ll be getting the angle with respect to a point in space. If so, then we can describe each argument as:

  • y-coordinate: The y-component of the point.
  • x-coordinate: The x-component of the point.

Notice how the y-component goes before the x-component.

What’s atan2()?

The atan2() function is the inverse of the tan() trigonometric function, so given a value, we can use it to get the original angle used in tan(). But how is it any different from the atan() function, Which is also the inverse for tan()? Well, to understand atan2() and why we need two inverse functions, we’ll have to look at the motivation behind atan().

Many times, while working with trigonometric functions, we are given a point in space in polar coordinates, meaning it’s defined by a radius (the distance from the center) and an angle (measured from the positive x-axis):

A point located on an x-y plane using polar coordinates. An angle is labeled between the established line and the x-axis.

If we want to convert into Cartesian coordinates (i.e., measuring the point’s position on the X and Y axes), then it’s as easy as using the cos() and sin() functions, which return the X and Y coordinates, respectively.

sin() and cos() only return the correct X and Y coordinates when multiplied by the radius. However, since we’ll be working with their quotient, we can ignore it.

However, what if we want to go the other way around? For example, given the following point in space, how could we find its angle?

A point on the x-y plane using Cartesian coordinates at 4, 3. A line extends from the center of the axes to the point.

Well, then we’ll have to remember the mathematical definition of tan(), which is given as the quotient of sin() over cos():

The tangent of an angle is equal to the sine of the angle divided by the cosine of the angle.

And as we mentioned, sin() and cos() return the x and y coordinates, so we can substitute them into the equation.

The tangent of an angle is equal to Y divided by X.

Now, applying atan() on both sides, we can solve for the angle:

An angle is equal to the a-tangent times the value of the y-coordinate divided by the x-coordinate.

That’s it! To find the angle given two points: divide Y by X and pass it into atan().

This will work great as long as we are working with positive quantities, since atan() is prone to confusing points. Remember, it’s a quotient, so if both coordinates are negative, it will confuse them as positive, and it will also confuse one negative coordinate with another.

Two math formulas. The first is negative Y divided by negative X equals Y divided by X. The second is negative Y divided by X is equal to Y divided by negative X.

To fix this, we will need to use atan2(), which takes Y and X as separate arguments so it is capable of finding the correct point and returning angles everywhere on the plane. This is why atan() output is capped between -90deg and 90deg, but atan2() can return any angle between -180deg and 180deg!

Basic usage

One of the most common uses for atan2() is rotating an element (by an angle) based on the user’s mouse position (given in X-Y coordinates). So, for example, imagine we want to point an element towards the user’s mouse; in my case, a simple arrow emoji.

<span> ➡️ </span>

We’ll need a little JavaScript to tell CSS the mouse position at all times:

const body = document.querySelector("body");

body.addEventListener("pointermove", (event) => {
  let x = event.clientX;
  let y = event.clientY;

  document.documentElement.style.setProperty("--m-x", `${Math.round(x)}px`);
  document.documentElement.style.setProperty("--m-y", `${Math.round(y)}px`);
});

Every time the user moves the mouse, its coordinates will be saved in the --m-y and --m-x custom properties.

And to rotate the arrow correctly, we’ll pass --m-y and --m-x (in that order) to atan2(), which will translate the mouse position into an angle that we can use for the rotation.

span {
  /*  To center the emoji and apply rotation  */
  display: flex;
  align-items: center;
  justify-content: center;

  rotate: atan2(var(--m-y), var(--m-x));
}

body {
  height: 100dvh;
  padding: var(--spacing);
}

Move your mouse around the page, and the arrow will follow!

Moving the origin

The last example was pretty straightforward: we passed --m-y and --m-x as-is to atan2(), and it gave us the correct angle. Most times, though, it’s not so easy.

For example, try to slip your mouse between the arrow and the demo’s top-left corner. Do you see it? The arrow is unable to look behind itself because of how the mouse position is measured. Remember that JavaScript measures the screen from the top-left corner, meaning our origin is exactly there. So, when we try to place our mouse in the padding between the page and the arrow, CSS still thinks we are pointing towards the bottom-right.

To fix this, we’ll need to move the origin of our coordinate system on top of the arrow, which can be done while computing the value in JavaScript or on the go in CSS. If we pick the second option, then we’ll first create two new custom properties --mc-y and --mc-x, standing for mouse-centered X and mouse-centered Y.

And to perfectly place the origin, we’ll subtract the padding between the arrow and half of the span‘s size:

:root {
  --mc-y: calc(var(--m-y) - var(--spacing) - var(--span-size) / 2);
  --mc-x: calc(var(--m-x) - var(--spacing) - var(--span-size) / 2);
}

span {
  /* ... */
  rotate: atan2(var(--mc-y), var(--mc-x));
}

Voilà! It can turn all the way around.

Please, don’t think that moving the origin is an edge case; we’ll actually have to move the origin any time we wish to place our arrow somewhere other than the top-left corner. For example, what if we wanted to place it right smack dab in the center? Well, centering the arrow itself is really simple:

body {
  display: grid;
  place-items: center;
  /* ... */
}

However, to center the origin of our coordinate system, we’ll need to:

  1. Subtract --m-y from half the page’s height. This way, it will be positive on the top half of the page and negative on the bottom half.
  2. Subtract half the page’s width from --m-x. This way, it will be negative on the left half of the page and positive on the right half.
:root {
  /* ... */
  --mc-y: calc(50dvh - var(--m-y));
  --mc-x: calc(var(--m-x) - 50dvw);
}

With this, we’ll mimic a real Cartesian coordinate. BUT, you’ll notice now that the arrow is turning opposite from our mouse position! This happens since CSS rotates elements in a clockwise direction, but we measure angles counter-clockwise. In our first examples, we didn’t notice since it was used to our advantage, but from now on, we’ll have to correct it by multiplying the angle by -1:

span {
  /* ... */
  rotate: calc(atan2(var(--mc-y), var(--mc-x)) * -1);
}

To move the origin somewhere else to, say, the bottom-right corner, we’ll do the following translation:

:root {
  --mc-y: calc(100dvh - var(--m-y) - var(--spacing) - var(--span-size) / 2);
  --mc-x: calc(var(--m-x) + var(--spacing) + var(--span-size) / 2 - 100dvw);
}

This works similarly to how we centered the origin, but this time going all the way to the bottom-right corner.

Notable Values

As we mentioned earlier, atan2() can take two values between -Infinity and Infinity. In math, Infinity can seem unpredictable at times, so the table below shows how atan2() should behave for different combinations of Infinity.

X
-∞-Finite0-0++Finite+∞
-∞-135deg-90deg-90deg-90deg-90deg-45deg
-Finite-180deg(normal)-90deg-90deg(normal)0deg
Y0--180deg(normal)-180deg0deg0deg0deg
0+180deg180deg180deg0deg0deg0deg
0+180deg180deg180deg0deg0deg0deg
+Finite180deg(normal)90deg90deg(normal)0deg
+∞135deg90deg90deg90deg90deg45deg

Demo

Specification

The atan2() function is defined in the CSS Values and Units Module Level 4 specification, which is currently in Editor’s Draft.

Browser support

More information