How To: Cursor Tracking Parallax
Tags: javascript, css, beginners, tutorial
Published: 2021-02-18 14:51:25

Ever seen those effects where elements on the screen respond to mouse movement? They're used quite often for parallax-like effects or eyes following a cursor. I use the effect on [jhey.dev](https://jhey.dev) for the shades glare on the main bear head.

I don't know the technical name. Let's go with "Cursor Tracking Parallax".

The good news is this technique doesn't take much time to make and adds a little something extra to your designs. Remember, it's the little details.

* * *

Let's make a face! We'll start with some markup.

```html
<div class="face">
  <div class="face__eyes">
    <div class="eye eye--left"></div>
    <div class="eye eye--right"></div>
  </div>
  <div class="face__mouth"></div>
</div>
```

And we've gone ahead and styled it up 💅

{% codepen https://codepen.io/jh3y/pen/ExNXNjN %}

> Remember, you can view the compiled HTML, CSS, and JavaScript in CodePen. Use the dropdown for a source panel and press "View Compiled HTML/CSS/JavaScript".</p>

That face is great. But, it'd be cooler if we could give it a little life.

To do this, we can use CSS variables with an event listener for "pointermove".

```javascript
document.addEventListener('pointermove', () => {
  // Make those features move by updating some CSS variables.
})
```

We want to limit the movement of those features though. We don't want them flying all over the place. We want "subtle".

Let's start by updating the CSS for our eyes container. That's **important**. We don't need to transition each eye. We're going to use scoped CSS variables in a `transform`.

```css
.face__eyes {
  transform: translate(calc(var(--x, 0) * 1px), calc(var(--y, 0) * 1px));
}
```

Note how we're using `calc` with the value of `1px`. It's not a bad habit to leave some CSS variables unitless. This gives us room to change to a different unit with ease.

There's no change yet though. The `--x` and `--y` values will fallback to `0`. But, you can play with this demo to see how updating the value would affect the eyes.

{% codepen https://codepen.io/jh3y/pen/QWGgGmb %}

Now. How about those scripts? We need a function that maps our cursor position to some defined range and outputs a value for us.

![A diagram showing "50" passing through the ranges and coming out as "750"](https://res.cloudinary.com/jh3yy/image/upload/f_auto,q_auto/v1613659704/jhey.dev/transformer_duetyf.jpg)

To do this we can create a mapping function.

```javascript
const mapRange = (inputLower, inputUpper, outputLower, outputUpper) => {
  const INPUT_RANGE = inputUpper - inputLower
  const OUTPUT_RANGE = outputUpper - outputLower
  return value => outputLower + (((value - inputLower) / INPUT_RANGE) * OUTPUT_RANGE || 0)
}
```

Our mapping function takes an input range and an output range. Then it returns a function we can use to map one value to another.

Let's run through what's happening there. We pass the function two ranges for input and output. After calculating the range, we return a function. The function does the magic.

1. Calculates the fractional value of an input value against the input range.
2. Multiplies that by the output range.
3. Add that to the lower bounds of the output range.

Consider this example with an input range of 0 to 100, an output range of 500 to 1000, and an input of 50.

```javascript
50 => 500 + (((50 - 0) / 100) * 500))
50 => 500 + (0.5 * 500)
50 => 500 + 250
50 => 750
```

We need to tie this up to our CSS variable transform and we're there! Here's how we can hook up the x translation for our eyes.

```javascript
const BOUNDS = 20
const update = ({ x, y }) => {
  const POS_X = mapRange(0, window.innerWidth, -BOUNDS, BOUNDS)(x)
  EYES.style.setProperty('--x', POS_X)
}
document.addEventListener('pointermove', update)
```

And that works!

{% codepen https://codepen.io/jh3y/pen/ExNXZKR %}

All that's left to do is hook up the other axis and features. Notice how we are declaring a "BOUNDS" that we use. For the y-axis, we follow the same procedure with `window.innerHeight` as an input.

{% codepen https://codepen.io/jh3y/pen/jOVwyVm %}

But, what about the mouth? Well, this is where the power of scoped CSS variables comes in.

Instead of setting the style on the eyes container, let's set it on the face element itself.

```javascript
const FACE = document.querySelector('.face')
const update = ({ x, y }) => {
  const POS_X = mapRange(0, window.innerWidth, -BOUNDS, BOUNDS)(x)
  const POS_Y = mapRange(0, window.innerHeight, -BOUNDS, BOUNDS)(y)
  FACE.style.setProperty('--x', POS_X)
  FACE.style.setProperty('--y', POS_Y)
}
```

Making those changes [won't break anything](https://codepen.io/jh3y/pen/rNWwjyv). That's CSS variable scope at work. The variable values will cascade down to the eyes container still. But now the mouth also has access and we can use the same transform on it. The starting `translateX` is used to center the mouth before another translation.

```css
.face__mouth {
  transform: translateX(-50%) translate(calc(var(--x, 0) * 1px), calc(var(--y, 0) * 1px));
}
```

And now the mouth moves too!

{% codepen https://codepen.io/jh3y/pen/NWbgdgd %}

But, it doesn't look right. It moves in sync with the eyes which feels a little off. This is one of those "attention to detail" things that's worth picking up. For example, if our faces had ears and the eyes went up, where would the ears go? Down! Check it in a mirror, I won't judge you. I've done stranger things for "details" 😅

How do we solve this then? Well, remember how I mentioned using `calc` with a unitless value back near the start? That comes in handy now.

We've implemented our JavaScript and CSS in a way that they have a separation of concerns. That's good! Our JavaScript is working out the cursor mapping range and passing it to our CSS. It doesn't care what we do with it there. In fact, the "BOUNDS" could be a nice round number like `100` and we could do what we please with it on the CSS side.

The individual features of the face handle their own transforms. Currently, they both use a coefficient of `1px`.

```css
.face__eyes {
  transform: translate(calc(var(--x, 0) * 1px), calc(var(--y, 0) * 1px));
}
.face__mouth {
  transform: translateX(-50%) translate(calc(var(--x, 0) * 1px), calc(var(--y, 0) * 1px));
}
```

But, what if we changed the mouth to use a coefficient of `-1px`?

```css
.face__mouth {
  transform: translateX(-50%) translate(calc(var(--x, 0) * -1px), calc(var(--y, 0) * -1px));
}
```

Now the mouth moves in the opposite direction to the eyes.

{% codepen https://codepen.io/jh3y/pen/wvoegjJ %}

But, we only had to change the coefficient in our CSS. That's one of the superpowers of using scoped CSS variables but keeping them unitless. We can power a scene with one variable whilst maintaining a good separation of concerns.

Make a couple of tweaks and we have a cursor tracking face using CSS variables!

{% codepen https://codepen.io/jh3y/pen/jOVwyQY %}

But, you don't need to only use it on faces. You can use it for lots of things. Another "nice touch" is creating parallax-like icon backgrounds with it. The trick there is to update the `background-position` with CSS variables.

I'm using that effect in this demo. It's not the "main event". But, it's nice little extra.

⚠️ This demo contains audio ⚠️

{% codepen https://codepen.io/jh3y/pen/yLVaJML %}

Here's a standalone version of an icon background to play with. The trick is to create a tile with an icon you like and then lean on `background-repeat`.

{% codepen https://codepen.io/jh3y/pen/MWboJdq %}

In this demo, you can configure the coefficient. This plays on the fact that we are separating concerns and letting CSS do what it wants with the value.

## That's it!

This is one way you can do "Cursor Tracking Parallax" with JavaScript and scoped CSS variables. I'm excited to see what you do with these techniques. What else could you make move? As always, [let me know](https://twitter.com/jh3yy) what you think and see you for the next one!

All the code is available in this [CodePen Collection](https://codepen.io/collection/npEvBv).

**Stay Awesome! ʕ •ᴥ•ʔ**