Parallax Powered by CSS Custom Properties
Tags: javascript, tutorial, webdev, design
Published: 2022-01-10 15:20:11

Good friend Kent C. Dodds has recently dropped his [new website](https://kentcdodds.com/) which had a lot of work go into it. I was fortunate enough that Kent reached out a while back and asked if I could come up with some "whimsy" for the site. ✨

One of the first things that drew my attention was the large image of Kody (🐨) on the landing page. He's surrounded by objects and that, to me, screamed, "Make me move!"

![Kody surrounded by blue things](https://res.cloudinary.com/jh3yy/image/upload/c_scale,f_auto,q_auto,w_1886/v1641823594/Projects/kody-flying_blue_kspk7s.png)

I have built parallax-style scenes before that respond to cursor movement, but not to this scale and not for a React application. The neat thing about this? We can power the whole thing with only two CSS custom properties.

* * *

Let's start by grabbing our user's cursor position. This is as straightforward as:

```javascript
const UPDATE = ({ x, y }) => {
  document.body.innerText = `x: ${x}; y: ${y}`
}
document.addEventListener('pointermove', UPDATE)
```

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

We want to map these values around a center point. For example, the left side of the viewport should be `-1` for `x`, and `1` for the right side. We can reference an element and work out the value from its center using a mapping function. In this project, I was able to use [GSAP](https://greensock.com/gsap/) and that meant using some of its utility functions. They already provide a `mapRange()` function for this purpose. Pass in two ranges and you'll get a function you can use to get the mapped value.

```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)
}
// const MAPPER = mapRange(0, 100, 0, 10000)
// MAPPER(50) === 5000
```

What if we want to use the window as the container element? We can map the value to the width and height of it.

```javascript
import gsap from 'https://cdn.skypack.dev/gsap'

const BOUNDS = 100

const UPDATE = ({ x, y }) => {
  const boundX = gsap.utils.mapRange(0, window.innerWidth, -BOUNDS, BOUNDS, x)
  const boundY = gsap.utils.mapRange(0, window.innerHeight, -BOUNDS, BOUNDS, y)
  document.body.innerText = `x: ${Math.floor(boundX) / 100}; y: ${Math.floor(boundY) / 100};`
}

document.addEventListener('pointermove', UPDATE)
```

That gives us a range of `x` and `y` values that we can plug into our CSS. Note how we are dividing the values by `100` to get a fractional value. This should make sense when we integrate these values with our CSS a little later.

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

Now, what if we have an element that we want to map that value against, and within a certain proximity? In other words, we want our handler to look up the position of the element, work out the proximity range, and then map the cursor position to that range. The ideal solution here is to create a function that generates our handler for us. Then we can reuse it. For the purpose of this article, though, we’re operating on a “happy path” where we are avoiding type checks or checking for the callback value, etc.

```javascript
const CONTAINER = document.querySelector('.container')

const generateHandler = (element, proximity, cb) => ({x, y}) => {
  const bounds = 100
  const elementBounds = element.getBoundingClientRect()
  const centerX = elementBounds.left + elementBounds.width / 2
  const centerY = elementBounds.top + elementBounds.height / 2
  const boundX = gsap.utils.mapRange(centerX - proximity, centerX + proximity, -bounds, bounds, x)
  const boundY = gsap.utils.mapRange(centerY - proximity, centerY + proximity, -bounds, bounds, y)
  cb(boundX / 100, boundY / 100)
}

document.addEventListener('pointermove', generateHandler(CONTAINER, 100, (x, y) => {
  CONTAINER.innerText = `x: ${x.toFixed(1)}; y: ${y.toFixed(1)};`
}))
```

In this demo, our proximity is `100` . We’ll style it with a blue background to make it obvious. We pass a callback that gets fired each time the values for `x` and `y` get mapped to the `bounds`. We can divide these values in the callback or do what we want with them.

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

But wait, there's an issue with that demo. The values go outside the bounds of `-1` and `1`. We need to clamp those values. GreenSock has another utility method we can use for this. It's the equal of using a combination of `Math.min` and `Math.max`. As we already have the dependency, there's no point in reinventing the wheel! We could clamp the values in the function. But, choosing to do so in our callback will be more flexible as we’ll show coming up.

We could do this with CSS `clamp()` if we’d like. 😉

```javascript
document.addEventListener('pointermove', generateHandler(CONTAINER, 100, (x, y) => {
  CONTAINER.innerText = `
    x: ${gsap.utils.clamp(-1, 1, x.toFixed(1))};
    y: ${gsap.utils.clamp(-1, 1, y.toFixed(1))};
  `
}))
```

Now we have clamped values!

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

In this demo, adjust the proximity and drag the container around to see how the handler holds up.

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

That's the majority of JavaScript for this project! All that's left to do is pass these values to CSS-land. And we can do that in our callback. Let’s use custom properties named `ratio-x` and `ratio-y`.

```javascript
const UPDATE = (x, y) => {
  const clampedX = gsap.utils.clamp(-1, 1, x.toFixed(1))
  const clampedY = gsap.utils.clamp(-1, 1, y.toFixed(1))
  CONTAINER.style.setProperty('--ratio-x', clampedX)
  CONTAINER.style.setProperty('--ratio-y', clampedY)
  CONTAINER.innerText = `x: ${clampedX}; y: ${clampedY};`
}

document.addEventListener('pointermove', generateHandler(CONTAINER, 100, UPDATE))
```

Now that we have some values we can use in our CSS, we can combine them with `calc``()` any way we like. For example, this demo changes the scale of the container element based on the `y` value. It then updates the `hue` of the container based on the `x` value.

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

The neat thing here is that the JavaScript doesn't care about what you do with the values. It's done its part. That's the magic of using scoped custom properties.

```css
.container {
  --hue: calc(180 - (var(--ratio-x, 0) * 180));
  background: hsl(var(--hue, 25), 100%, 80%);
  transform: scale(calc(2 - var(--ratio-y, 0)));
}
```

Another interesting point is considering whether you want to clamp the values or not. In this demo, if we didn't clamp `x`, we could have the `hue` update wherever we are on the page.

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

## Making a scene

We have the technique in place! Now we can do pretty much whatever we want with it. It's kinda wherever your imagination takes you. I've used this same set up for a bunch of things.

Our demos so far have only made changes to the containing element. But, as we may as well mention again, [the power of custom property scope is epic](https://css-tricks.com/the-power-and-fun-of-scope-with-css-custom-properties/).

My task was to make things move on Kent’s site. When I first saw the image of Kody with a bunch of objects, I could see all the individual pieces doing their own thing—all powered by those two custom properties that we pass in. How might that look though? The key is inline custom properties for each child of our container.

For now, we could update our markup to include some children:

```html
<div class="container">
  <div class="container__item"></div>
  <div class="container__item"></div>
  <div class="container__item"></div>
</div>
```

Then we update the styles to include some scoped styles for `container__item`:

```css
.container__item {
  position: absolute;
  top: calc(var(--y, 0) * 1%);
  left: calc(var(--x, 0) * 1%);
  height: calc(var(--size, 20) * 1px);
  width: calc(var(--size, 20) * 1px);
  background: hsl(var(--hue, 0), 80%, 80%);
  transition: transform 0.1s;
  transform:
    translate(-50%, -50%)
    translate(
      calc(var(--move-x, 0) * var(--ratio-x, 0) * 100%),
      calc(var(--move-y, 0) * var(--ratio-y, 0) * 100%)
    )
    rotate(calc(var(--rotate, 0) * var(--ratio-x, 0) * 1deg))
  ;
}
```

The important part there is how we're making use of `--ratio-x` and `--ratio-y` inside the `transform`. Each item declares its own level of movement and rotation via `--move-x` , etc. Each item is also positioned with scoped custom properties, `--x` and `--y`.

**That's the key to these CSS powered parallax scenes. It's all about bouncing coefficients against each other!**

If we update our markup with some inline values for those properties, here’s what we get:

```html
<div class="container">
  <div class="container__item" style="--move-x: -1; --rotate: 90; --x: 10; --y: 60; --size: 30; --hue: 220;"></div>
  <div class="container__item" style="--move-x: 1.6; --move-y: -2; --rotate: -45; --x: 75; --y: 20; --size: 50; --hue: 240;"></div>
  <div class="container__item" style="--move-x: -3; --move-y: 1; --rotate: 360; --x: 75; --y: 80; --size: 40; --hue: 260;"></div>
</div>
```

Leveraging that scope, we can get something like this! That’s pretty neat. It almost looks like a shield.

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

But, how do you take a static image and turn it into a responsive parallax scene? First, we're going to have to create all those child elements and position them. And to do this we can use the ["tracing" technique we use with CSS art](https://css-tricks.com/advice-for-complex-css-illustrations/#tracing-is-perfectly-acceptable).

This next demo shows the image we’re using inside a parallax container with children. To explain this part, we've created three children and given them a red background. The image is `fixed` with a reduced `opacity` and lines up with our parallax container.

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

Each parallax item gets created from a `CONFIG` object. For this demo, I'm using Pug to generate these in HTML for brevity. In the final project, I'm using React which we can show later. Using Pug here saves me writing out all the inline CSS custom properties individually.

```pug
-
  const CONFIG = [
    {
      positionX: 50,
      positionY: 55,
      height: 59,
      width: 55,
    },
    {
      positionX: 74,
      positionY: 15,
      height: 17,
      width: 17,
    },
    {
      positionX: 12,
      positionY: 51,
      height: 24,
      width: 19,
    }
  ]

img(src="https://assets.codepen.io/605876/kody-flying_blue.png")
.parallax
  - for (const ITEM of CONFIG)
    .parallax__item(style=`--width: ${ITEM.width}; --height: ${ITEM.height}; --x: ${ITEM.positionX}; --y: ${ITEM.positionY};`)
```

How do we get those values? It's a lot of trial and error and is definitely time consuming. To make it responsive, the positioning and sizing use percentage values.

```css
.parallax {
  height: 50vmin;
  width: calc(50 * (484 / 479) * 1vmin); // Maintain aspect ratio where 'aspect-ratio' doesn't work to that scale.
  background: hsla(180, 50%, 50%, 0.25);
  position: relative;
}

.parallax__item {
  position: absolute;
  left: calc(var(--x, 50) * 1%);
  top: calc(var(--y, 50) * 1%);
  height: calc(var(--height, auto) * 1%);
  width: calc(var(--width, auto) * 1%);
  background: hsla(0, 50%, 50%, 0.5);
  transform: translate(-50%, -50%);
}
```

Once we’ve made elements for all the items, we get something like the following demo. This uses the config object from the final work:

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

Don't worry if things aren’t perfectly lined up. Everything is going to be moving anyway! That's the joy of using a config object—we get tweak it how we like.

How do we get the image into those items? Well, it’s tempting to create separate images for each item. But, that would result in a lot of network requests for each image which is bad for performance. Instead, we can create an image sprite. In fact, that’s exactly what I did.

![Kody Sprite](https://paper-attachments.dropbox.com/s_F0AB372D4DA74E76CD90E65F3A36BCC2547E02186F1F31F213144B72371DA864_1636738778701_kody-spritesheet-all_ak8clo.webp)

Then to keep things responsive, we can use a percentage value for the `background-size` and `background-position` properties in the CSS. We make this part of the config and then inline those values, too. The config structure can be anything.

```pug
-
  const ITEMS = [
    {
      identifier: 'kody-blue',
      backgroundPositionX: 84.4,
      backgroundPositionY: 50,
      size: 739,
      config: {
        positionX: 50,
        positionY: 54,
        height: 58,
        width: 55,
      },
    },
  ]

.parallax
  - for (const ITEM of ITEMS)
    .parallax__item(style=`--pos-x: ${ITEM.backgroundPositionX}; --pos-y: ${ITEM.backgroundPositionY}; --size: ${ITEM.size}; --width: ${ITEM.config.width}; --height: ${ITEM.config.height}; --x: ${ITEM.config.positionX}; --y: ${ITEM.config.positionY};`)
```

Updating our CSS to account for this:

```css
.parallax__item {
  position: absolute;
  left: calc(var(--x, 50) * 1%);
  top: calc(var(--y, 50) * 1%);
  height: calc(var(--height, auto) * 1%);
  width: calc(var(--width, auto) * 1%);
  transform: translate(-50%, -50%);
  background-image: url("kody-sprite.png");
  background-position: calc(var(--pos-x, 0) * 1%) calc(var(--pos-y, 0) * 1%);
  background-size: calc(var(--size, 0) * 1%);
}
```

And now we have a responsive traced scene with parallax items!

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

All that's left to do is remove the tracing image and the background colors, and apply transforms.

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

In the first version, I used the values in a different way. I had the handler return values between `-60` and `60`. We can do that with our handler by manipulating the return values.

```javascript
const UPDATE = (x, y) => {
  CONTAINER.style.setProperty(
    '--ratio-x',
    Math.floor(gsap.utils.clamp(-60, 60, x * 100))
  )
  CONTAINER.style.setProperty(
    '--ratio-y',
    Math.floor(gsap.utils.clamp(-60, 60, y * 100))
  )
}
```

Then, each item can be configured for:

- the x, y, and z positions,
- movement on the x and y axis, and
- rotation and translation on the x and y axis.

The CSS transforms are quite long. This is what they look like:

```css
.parallax {
  transform: rotateX(calc(((var(--rx, 0) * var(--range-y, 0)) * var(--allow-motion)) * 1deg))
    rotateY(calc(((var(--ry, 0) * var(--range-x, 0)) * var(--allow-motion)) * 1deg))
    rotate(calc(((var(--r, 0) * var(--range-x, 0)) * var(--allow-motion)) * 1deg));
  transform-style: preserve-3d;
  transition: transform 0.25s;
}

.parallax__item {
  transform: translate(-50%, -50%)
    translate3d(
      calc(((var(--mx, 0) * var(--range-x, 0)) * var(--allow-motion)) * 1%),
      calc(((var(--my, 0) * var(--range-y, 0)) * var(--allow-motion)) * 1%),
      calc(var(--z, 0) * 1vmin)
    )
    rotateX(calc(((var(--rx, 0) * var(--range-y, 0)) * var(--allow-motion)) * 1deg))
    rotateY(calc(((var(--ry, 0) * var(--range-x, 0)) * var(--allow-motion)) * 1deg))
    rotate(calc(((var(--r, 0) * var(--range-x, 0)) * var(--allow-motion)) * 1deg));
  transform-style: preserve-3d;
  transition: transform 0.25s;
}
```

What’s that `--allow-motion` thing doing? That’s not in the demo! True. This is a little trick for applying reduced motion. If we have [users who prefer "reduced" motion](https://css-tricks.com/revisiting-prefers-reduced-motion-the-reduced-motion-media-query/), we can cater for that with a coefficient. The word "reduced" doesn't have to mean "none" after all!

```css
@media (prefers-reduced-motion: reduce) {
  .parallax {
    --allow-motion: 0.1;
  }
}
@media (hover: none) {
  .parallax {
    --allow-motion: 0;
  }
}
```

This "final" demo shows how the `--allow-motion` value affects the scene. Move the slider to see how you can reduce the motion.

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

This demo also shows off another feature: the ability to choose a "team" that changes Kody’s color. The neat part here is that all that requires is pointing to a different part of our image sprite.

And that's it for creating a CSS custom property powered parallax! But, I did mention this was something I built in React. And yes, that last demo uses React. In fact, this worked quite well in a component-based environment. We have an array of configuration objects and we can pass them into a `<Parallax>` component as `children` along with any transform coefficients.

```javascript
const Parallax = ({
  config,
  children,
}: {
  config: ParallaxConfig
  children: React.ReactNode | React.ReactNode[]
}) => {
  const containerRef = React.useRef<HTMLDivElement>(null)
  useParallax(
    (x, y) => {
      containerRef.current.style.setProperty(
        '--range-x', Math.floor(gsap.utils.clamp(-60, 60, x * 100))
      )
      containerRef.current.style.setProperty(
        '--range-y', Math.floor(gsap.utils.clamp(-60, 60, y * 100))
      )
    },
    containerRef,
    () => window.innerWidth * 0.5,
)

  const containerStyle = {
    '--r': config.rotate,
    '--rx': config.rotateX,
    '--ry': config.rotateY,
  }
  return (
    <div
      ref={containerRef}
      className="parallax"
      style={
        containerStyle as ContainerCSS
      }
    >
      {children}
    </div>
  )
}
```

Then, if you spotted it, there's a hook in there called `useParallax`. We pass a callback into this that receives the `x` and `y` value. We also pass in the `proximity` which can be a `function`, and the element to use.

```javascript
const useParallax = (callback, elementRef, proximityArg = 100) => {
  React.useEffect(() => {
    if (!elementRef.current || !callback) return
    const UPDATE = ({ x, y }) => {
      const bounds = 100
      const proximity = typeof proximityArg === 'function' ? proximityArg() : proximityArg
      const elementBounds = elementRef.current.getBoundingClientRect()
      const centerX = elementBounds.left + elementBounds.width / 2
      const centerY = elementBounds.top + elementBounds.height / 2
      const boundX = gsap.utils.mapRange(centerX - proximity, centerX + proximity, -bounds, bounds, x)
      const boundY = gsap.utils.mapRange(centerY - proximity, centerY + proximity, -bounds, bounds, y)
      callback(boundX / 100, boundY / 100)
    }
    window.addEventListener('pointermove', UPDATE)
    return () => {
      window.removeEventListener('pointermove', UPDATE)
    }
  }, [elementRef, callback])
}
```

Spinning this into a custom hook means I can reuse it elsewhere. In fact, removing the use of GSAP makes it a nice micro-package opportunity.

Lastly, the `<ParallaxItem>`. This is pretty straightforward. It's a component that maps the props into inline CSS custom properties. In the project, I opted to map the `background` properties to a child of the `ParallaxItem`.

```javascript
const ParallaxItem = ({
  children,
  config,
}: {
  config: ParallaxItemConfig
  children: React.ReactNode | React.ReactNode[]
}) => {
  const params = {...DEFAULT_CONFIG, ...config}
  const itemStyle = {
    '--x': params.positionX,
    '--y': params.positionY,
    '--z': params.positionZ,
    '--r': params.rotate,
    '--rx': params.rotateX,
    '--ry': params.rotateY,
    '--mx': params.moveX,
    '--my': params.moveY,
    '--height': params.height,
    '--width': params.width,
  }
  return (
    <div
      className="parallax__item absolute"
      style={
        itemStyle as ItemCSS
      }
    >
      {children}
    </div>
  )
}
```

Tie all that together and you could end up with something like this:

```javascript
const ITEMS = [
  {
    identifier: 'kody-blue',
    backgroundPositionX: 84.4,
    backgroundPositionY: 50,
    size: 739,
    config: {
      positionX: 50,
      positionY: 54,
      moveX: 0.15,
      moveY: -0.25,
      height: 58,
      width: 55,
      rotate: 0.01,
    },
  },
  ...otherItems
]

const KodyConfig = {
  rotate: 0.01,
  rotateX: 0.1,
  rotateY: 0.25,
}

const KodyParallax = () => (
  <Parallax config={KodyConfig}>
    {ITEMS.map(item => (
      <ParallaxItem key={item.identifier} config={item.config} />
    ))}
  </Parallax>
)
```

Which gives us our parallax scene!

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

## That’s it!

We just took a static image and turned it into a slick parallax scene powered by CSS custom properties! It’s funny because image sprites have been around a long time, but they still have a lot of use today!

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