Parallax Effect with Framer Motion

Published
· 5 months ago
framer motionparallaxreactanimationtypescript
Framer Motion is motion library for React that makes it easy to add beautiful animation to your apps.
In this article, we'll see how to create a parallax effect with Framer Motion in a few lines of code. We will be using Vite for tooling and TailwindCSS for styling, but you can use any build tool and any CSS framework or no framework at all.

What is a Parallax Effect?

A parallax effect is a visual effect where elements on the page move at different speeds when the user scrolls, which can give the illusion of depth. A great example of this is the Firewatch game website.
Firewatch Game Website
The example we will be creating is much simpler and not as impressive as this one, but it should give you a good idea of how the parallax effect works and how to implement it.

Setting up the Project

We will be using a simple Vite project for this tutorial.
We start by creating a new Vite project with the following command:
# npm 6.x
npm create vite@latest parallax --template react-ts
 
# npm 7+, extra double-dash is needed:
npm create vite@latest parallax -- --template react-ts
 
cd parallax
npm install
# npm 6.x
npm create vite@latest parallax --template react-ts
 
# npm 7+, extra double-dash is needed:
npm create vite@latest parallax -- --template react-ts
 
cd parallax
npm install
Then, install Framer Motion and Tailwind CSS:
npm install framer-motion
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
npm install framer-motion
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Next, replace the contents of /src/index.css with the following:
/src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
/src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
And the contents of tailwind.config.js with the following:
tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
  theme: {
    extend: {},
  },
  plugins: [],
};
tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
  theme: {
    extend: {},
  },
  plugins: [],
};

Creating the Parallax Effect

Let's create a new component file called parallax.tsx inside /src/components and add the following code:
/src/components/parallax.tsx
import { motion } from 'framer-motion';
 
export default function Parallax() {
  return (
    <motion.div className="flex h-1/2 w-2/3 flex-row gap-2">
      <motion.div className="w-1/3 bg-rose-600" />
      <motion.div className="w-1/3 bg-amber-600" />
      <motion.div className="w-1/3 bg-emerald-600" />
    </motion.div>
  );
}
/src/components/parallax.tsx
import { motion } from 'framer-motion';
 
export default function Parallax() {
  return (
    <motion.div className="flex h-1/2 w-2/3 flex-row gap-2">
      <motion.div className="w-1/3 bg-rose-600" />
      <motion.div className="w-1/3 bg-amber-600" />
      <motion.div className="w-1/3 bg-emerald-600" />
    </motion.div>
  );
}
This is a simple component that renders three boxes with different colors. We are using <motion.div><motion.div> instead of <div><div> so we can later add Framer Motion properties to these elements.
Now lets head to /src/App.tsx and place our new component inside the <App><App> component:
/src/App.tsx
import Parallax from './components/parallax';
 
function App() {
  return (
    <>
      <section className="flex h-screen w-screen items-center justify-center">
        <Parallax />
      </section>
      <section className="flex h-screen w-screen items-center justify-center">
        <Parallax />
      </section>
    </>
  );
}
 
export default App;
/src/App.tsx
import Parallax from './components/parallax';
 
function App() {
  return (
    <>
      <section className="flex h-screen w-screen items-center justify-center">
        <Parallax />
      </section>
      <section className="flex h-screen w-screen items-center justify-center">
        <Parallax />
      </section>
    </>
  );
}
 
export default App;
We created 2 sections, each taking up the full width and height of the screen and placed our <Parallax><Parallax> component inside each one of them.
Our app should look like this now:
Parallax step 1
Now let's add the parallax effect to our component.
We want the red box to move as normal as we scroll, the yellow box to move faster and the green box to move even faster.
To achieve this, we need to track the scroll progress of the wrapper div of our <Parallax><Parallax> component. We can do this using the useScrolluseScroll hook from Framer Motion:
/src/components/parallax.tsx
import { useRef } from 'react';
import { motion, useScroll } from 'framer-motion';
 
export default function Parallax() {
  const ref = useRef<HTMLDivElement>(null);
 
  const { scrollYProgress } = useScroll({ target: ref });
 
  return (
    <motion.div ref={ref} className="flex h-1/2 w-2/3 flex-row gap-2">
      <motion.div className="w-1/3 bg-rose-600" />
      <motion.div className="w-1/3 bg-amber-600" />
      <motion.div className="w-1/3 bg-emerald-600" />
    </motion.div>
  );
}
/src/components/parallax.tsx
import { useRef } from 'react';
import { motion, useScroll } from 'framer-motion';
 
export default function Parallax() {
  const ref = useRef<HTMLDivElement>(null);
 
  const { scrollYProgress } = useScroll({ target: ref });
 
  return (
    <motion.div ref={ref} className="flex h-1/2 w-2/3 flex-row gap-2">
      <motion.div className="w-1/3 bg-rose-600" />
      <motion.div className="w-1/3 bg-amber-600" />
      <motion.div className="w-1/3 bg-emerald-600" />
    </motion.div>
  );
}
We created a refref for the wrapper div and passed it to the useScrolluseScroll hook as the target element.
Now scrollYProgressscrollYProgress tracks the scroll progress of the wrapper div: when it's at the top of the screen, the value is 0, and the value will transition to 1 as it reaches the bottom of the screen.
Next, we define a function called useParallaxuseParallax:
/src/components/parallax.tsx
function useParallax(value: MotionValue<number>, distance: number) {
  return useTransform(value, [0, 1], [-distance, distance]);
}
/src/components/parallax.tsx
function useParallax(value: MotionValue<number>, distance: number) {
  return useTransform(value, [0, 1], [-distance, distance]);
}
To understand what this function does, we need to understand what the useTransformuseTransform hook does.
function useTransform<I, O>(
  value: MotionValue<number>,
  inputRange: InputRange,
  outputRange: O[],
  options?: TransformOptions<O>,
): MotionValue<O>;
function useTransform<I, O>(
  value: MotionValue<number>,
  inputRange: InputRange,
  outputRange: O[],
  options?: TransformOptions<O>,
): MotionValue<O>;
useTransformuseTransform creates a MotionValueMotionValue that transforms the output of another MotionValueMotionValue. A motion value can be opacityopacity, scalescale, xx, yy, etc.
In the following example, the value of opacityopacity will be 1 when xx is 0, transition to 0 when xx is 100 and transition back to 1 when xx is 200.
export const MyComponent = () => {
  const x = useMotionValue(0);
  const opacity = useTransform(x, [0, 100, 200], [1, 0, 1]);
 
  return <motion.div style={{ x, opacity }} />;
};
export const MyComponent = () => {
  const x = useMotionValue(0);
  const opacity = useTransform(x, [0, 100, 200], [1, 0, 1]);
 
  return <motion.div style={{ x, opacity }} />;
};
In our useParallaxuseParallax function, we are using useTransformuseTransform to transform the valuevalue passed to the function.
When valuevalue is 0, the output will be -distance, and it will transition to distance when value is 1.
The value we are going to pass to useParallaxuseParallax is the scrollYProgressscrollYProgress of the wrapper div:
/src/components/parallax.tsx
const yYellow = useParallax(scrollYProgress, 300);
const yGreen = useParallax(scrollYProgress, 600);
/src/components/parallax.tsx
const yYellow = useParallax(scrollYProgress, 300);
const yGreen = useParallax(scrollYProgress, 600);
This means that:
  • When the wrapper div is at the top of the screen, scrollYProgressscrollYProgress is 0, therefore yYellowyYellow and yGreenyGreen will be -300 and -600 respectively.
  • When the wrapper div is at the middle of the screen, scrollYProgressscrollYProgress is 0.5, therefore yYellowyYellow and yGreenyGreen will both be 0.
  • When the wrapper div is at the bottom of the screen, scrollYProgressscrollYProgress is 1, therefore yYellowyYellow and yGreenyGreen will be 300 and 600 respectively.
All we have to do now is pass the yYellowyYellow and yGreenyGreen values to the y property of the yellow and green boxes respectively:
/src/components/parallax.tsx
export default function Parallax() {
  // ...
  return (
    <motion.div ref={ref} className="flex h-1/2 w-2/3 flex-row gap-2">
      <motion.div className="w-1/3 bg-rose-600" />
      <motion.div
        className="w-1/3 bg-amber-600"
        initial={{ y: 0 }}
        style={{ y: yYellow }}
      />
      <motion.div
        className="w-1/3 bg-emerald-600"
        initial={{ y: 0 }}
        style={{ y: yGreen }}
      />
    </motion.div>
  );
}
/src/components/parallax.tsx
export default function Parallax() {
  // ...
  return (
    <motion.div ref={ref} className="flex h-1/2 w-2/3 flex-row gap-2">
      <motion.div className="w-1/3 bg-rose-600" />
      <motion.div
        className="w-1/3 bg-amber-600"
        initial={{ y: 0 }}
        style={{ y: yYellow }}
      />
      <motion.div
        className="w-1/3 bg-emerald-600"
        initial={{ y: 0 }}
        style={{ y: yGreen }}
      />
    </motion.div>
  );
}
We also set the inital value of yy to 0 so the boxes are positioned correctly when the page loads.
That's it! We have successfully added a parallax effect to our component.
Parallax
Here is the complete code for the <Parallax><Parallax> component:
/src/components/parallax.tsx
import { useRef } from 'react';
import {
  motion,
  useScroll,
  useTransform,
  type MotionValue,
} from 'framer-motion';
 
function useParallax(value: MotionValue<number>, distance: number) {
  return useTransform(value, [0, 1], [-distance, distance]);
}
 
export default function Parallax() {
  const ref = useRef<HTMLDivElement>(null);
 
  const { scrollYProgress } = useScroll({ target: ref });
 
  const yYellow = useParallax(scrollYProgress, 300);
  const yGreen = useParallax(scrollYProgress, 600);
 
  return (
    <motion.div ref={ref} className="flex h-1/2 w-2/3 flex-row gap-2">
      <motion.div className="w-1/3 bg-rose-600" />
      <motion.div
        className="w-1/3 bg-amber-600"
        initial={{ y: 0 }}
        style={{ y: yYellow }}
      />
      <motion.div
        className="w-1/3 bg-emerald-600"
        initial={{ y: 0 }}
        style={{ y: yGreen }}
      />
    </motion.div>
  );
}
/src/components/parallax.tsx
import { useRef } from 'react';
import {
  motion,
  useScroll,
  useTransform,
  type MotionValue,
} from 'framer-motion';
 
function useParallax(value: MotionValue<number>, distance: number) {
  return useTransform(value, [0, 1], [-distance, distance]);
}
 
export default function Parallax() {
  const ref = useRef<HTMLDivElement>(null);
 
  const { scrollYProgress } = useScroll({ target: ref });
 
  const yYellow = useParallax(scrollYProgress, 300);
  const yGreen = useParallax(scrollYProgress, 600);
 
  return (
    <motion.div ref={ref} className="flex h-1/2 w-2/3 flex-row gap-2">
      <motion.div className="w-1/3 bg-rose-600" />
      <motion.div
        className="w-1/3 bg-amber-600"
        initial={{ y: 0 }}
        style={{ y: yYellow }}
      />
      <motion.div
        className="w-1/3 bg-emerald-600"
        initial={{ y: 0 }}
        style={{ y: yGreen }}
      />
    </motion.div>
  );
}
2023 · Kfir Fitousi