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.
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:
<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:
: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:
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:
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:
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:
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.
.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:
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.
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.
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 article about 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.
Top comments (5)
No disrespect, but I think you need to change the title. I came here very interested to find out how CSS could pass cursor position even though CSS does not handle these events and so they would usually need to be handled by JS and then passed via JS to CSS. Your article then shows a standard JS method for handling and passing the data. CSS isn’t passing anything here. It is receiving from JS. I think it is fair to say this article is about JS passing the data to CSS variables instead of JS passing the data via direct declarations, right? Not CSS “passing” data, but just using a CSS variable instead of direct declaration.
Again, no disrespect intended at all, just want to help you avoid others coming here and finding the same thing and being grumpy with you about it. :)
I've actually just realised that on a whim and changed the title immediately! I have absolutely no intention to mislead anyone, because of course it isn't currently possible to actually catch any sort of event with CSS.
I hope this tiny change in the title makes things a lot clearer.
Yep! Perfect!
Thanks! This is very interesting. I think I can use this type of thing a lot.
No problem! There's a ton you can do with it! :)