Naseki/writings/cursor-position-with-css-variables
  • Light
  • Solar
  • Dark
  • OLED
  • Remedy
  • JetBrains Mono
  • IBM Plex Sans

Pass your cursor position to CSS variables with JavaScript

My website has a little illustration that tracks your cursor's position. The only JavaScript that happens there is passing the cursor position to CSS variables. Everything else is in CSS using 2D transforms. In this tutorial, you'll learn how to pass event behaviour to CSS using CSS variables.

Pass your cursor position to CSS variables with JavaScript

You can also read this post on dev.to.

(In case you don't know what illustration I'm talking about)

Set up your HTML and CSS

We'll be making a square with a little red dot inside. The red dot will be what will be controlled with transforms later on.

The HTML code is pretty simple:

HTML
<div class="container">
<div class="tracker"></div>
</div>

The CSS will center the square on the window and puts the red dot on the top left of the container. I also like setting up the CSS variables in CSS for a default state. CSS variables generally default to 0, but you may not always want that to be the case.

Here's the CSS:

CSS
:root {
--x: 0.5;
--y: 0.5;
}

.container, .tracker {
position: absolute;
}

.container {
width: 200px;
height: 200px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border: 3px solid #333;
}

.tracker {
width: 10px;
height: 10px;
left: 0;
top: 0;
background: red;
border-radius: 1000px;
}

This is how it ends up looking like:

Make the event listener and pass the variable in JavaScript

Next, we make the event listener and pass the event behaviour to CSS variables.

Let's declare the position x and y first, and add the event listener itself:

JavaScript
const pos = { x : 0, y : 0 };

document.addEventListener('mousemove', e => { saveCursorPosition(e.clientX, e.clientY); })

As you can see, we'll be making a function saveCursorPosition. This is where we'll be passing the CSS variables. The two arguments will be the event's clientX and clientY, which is the cursor position.

To make the cursor position responsive, we'll divide the values by the innerWidth and innerHeight, like so:

JavaScript
pos.x = (x / window.innerWidth).toFixed(2);
pos.y = (y / window.innerHeight).toFixed(2);

I use toFixed(2) to round the values.

After that, we can finally pass the positions to the CSS variables! To select the root, you use document.documentElement. You may be used to declaring CSS values using the style property, like style.transform = ''. This isn't possible with CSS variables, where you'll have to use style.setProperty() instead due to the unique way CSS variables are formatted. You'll end up with this:

JavaScript
document.documentElement.style.setProperty('--x', pos.x);
document.documentElement.style.setProperty('--y', pos.y);

Put these in your function, and you end up with this:

JavaScript
const pos = { x : 0, y : 0 };

const saveCursorPosition = function(x, y) {
pos.x = (x / window.innerWidth).toFixed(2);
pos.y = (y / window.innerHeight).toFixed(2);
document.documentElement.style.setProperty('--x', pos.x);
document.documentElement.style.setProperty('--y', pos.y);
}

document.addEventListener('mousemove', e => { saveCursorPosition(e.clientX, e.clientY); })

Add the transform with the CSS variables to your CSS

Now that we have everything ready, we'll now add the transform property to the red dot. This takes a little bit of math, but basically we'll multiply the container's width by the CSS variable, and add that to the -50% that centers the dot.

CSS
.tracker {
width: 10px;
height: 10px;
left: 0;
top: 0;
background: red;
border-radius: 1000px;
transform: translate(calc(-50% + 200px * var(--x)), calc(-50% + 200px * var(--y)));
}

Optionally, you can add a transition that acts as a stabiliser simply by adding this one line transition: transform 0.1s. It's best to keep the timing lower than 0.3s. I don't recommend using this on too many elements, but it adds a nice effect.

And that's it! The transform will now change according to the CSS variable, thus following your cursor. The final result can be seen on this JSFiddle (including the transition):

Why you should use CSS variables to pass event behaviour

Now you know how to pass event behaviour using CSS variables, but perhaps you still have doubts about CSS variables in general.

Readability

Readability in general is pretty subjective. But I do believe it's the major reason to make use of CSS variables.

My general thumb of rule is always: all style-related features should be left to CSS, everything else to JavaScript.

JS isn't meant for style manipulation. I mean, look at this:

JavaScript
const el = document.querySelector('.a-class');
const pos = { x : 1, y : 0.8 };

el.style.width = (250 * pos.x) + 'px';
el.style.height= (200 * pos.y) + 'px';
el.style.left= (100 * pos.x) + 'px';
el.style.top= (50 * pos.y) + 'px';
el.style.transform= `translate(${50 * pos.x}%, ${50 * pos.y}px)`;

It's just not very elegant, you see? You could use cssText so you'd at least not have this multiline However, this still isn't convenient when this isn't the only inline CSS you use. Besides, readability or maintainability won't be much better even with cssText.

It's also for the same reason that I prefer not to use libraries like GSAP and anime.js. Unless the animation is so complex that these libraries would outperform CSS, I'd rather go for CSS.

Speaking of performance...

Performance

This is where things get a little more complicated, but the tl;dr of this would be: It generally outperforms JS in terms of scripting.

Whether you pass the styling with CSS or JS makes no difference in terms of re-rendering itself. This means that you'll mainly find improved performance in JS itself.

Usually, whenever you want to change the position of an element, you'd do something like el.style.transform= 'translateY(50%)';.

Say you have 10000 divs, you'd loop through each element to add inline CSS to it. With CSS variables, you'd only need to change the value once on the parent element or the root. It should be clear that the latter would have better performance. In case you're doubting this, I did some benchmark testing using Jsben.ch. Some information about what I've done:

  • Before each test, I've created these 10000 divs, set the CSS variable, and added inline CSS to all of them with el.style.transform= 'translateY(var(--percent))';.
  • The first test case adds inline CSS with a regular variable.
  • The second test case changes the CSS variable.
The loop with regular variable can only do 84 operations when redeclaring the CSS variable can do a whopping 609023 operations!
The loop with regular variable can only do 84 operations when redeclaring the CSS variable can do a whopping 609023 operations!

That's a pretty big difference huh.

It may seem ridiculous to add inline styling to all these elements individually, but this is exactly what I see happening on many websites. Whereas in CSS variables you'd typically already have the CSS set beforehand in your stylesheet. All you'd need to do is change the CSS variable.

But hey, what if you use inline styling on both cases? That's when inline styling using a regular variable wins.

The loop with regular variable can do 83 operations and the loop with the CSS variable can only do 60 operations, making their speed closer than the previous case.
The loop with regular variable can do 83 operations and the loop with the CSS variable can only do 60 operations, making their speed closer than the previous case.

I don't see anyone ever doing this though...

When you use CSS variables for a ton of animations and transitions on your page, you might wanna start using JS for animation instead. This isn't so much a CSS variable problem as it is the performance of CSS animations in general. However, you can still use CSS variables to pass the CSS while doing the animation logic itself in JS! Here's a very short articleabout using CSS variables with GSAP.

How about browser support?

CSS variables are pretty widely used nowadays, and for a great reason too! All modern browsers support this feature at this point. There are just a few things to consider if you need to support legacy browsers as well:

  • Internet Explorer doesn't support CSS variables at all. If you still need to support IE, you could either opt for a polyfill, but at that point, you're better off just using JS.
  • Edge 15 (pre-Chromium) has a few bugs that may hinder you. But honestly, it's almost become impossible to keep Edge 15 installed, so there's very little reason to support it.

For more information, check out Can I Use.

A new world has just opened!

Now that you know how to pass these events, there's so much more you can do with it! If you want to make this work on touch devices, you can use the touchmove event for it. Try to play around with other events as well! You could make a complex parallax using JavaScript only to pass the scroll event values and CSS for everything else.

Not only do you know how to use it, but you also understand why you'd use this, and how this would improve performance.

Thanks for reading! 💻

If you wanna stay up to date with dev, subscribe to my newsletter! I send a couple of articles and resources once a week and will let you know when I've written a new article as well.

Not sure if it's for you? Read a sample newsletter here!

image
image

If you wanna stay up to date with dev, subscribe to my newsletter! I send a couple of articles and resources once a week and will let you know when I've written a new article as well.

Not sure if it's for you? Read a sample newsletter here!