Parallax Effect with Framer Motion
Published
· 2 years 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.
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.
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:
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
Our app should look like this now:
<Parallax>
<Parallax>
component inside each one of them.Our app should look like this now:
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.
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 useScroll
useScroll
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
Now
ref
ref
for the wrapper div and passed it to the useScroll
useScroll
hook as the target element.Now
scrollYProgress
scrollYProgress
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
useParallax
useParallax
:/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
useTransform
useTransform
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>;
useTransform
useTransform
creates a MotionValue
MotionValue
that transforms the output of another MotionValue
MotionValue
. A motion value can be opacity
opacity
, scale
scale
, x
x
, y
y
, etc.In the following example, the value of
opacity
opacity
will be 1
when x
x
is 0
, transition to 0
when x
x
is 100
and transition back to 1
when x
x
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
When
useParallax
useParallax
function, we are using useTransform
useTransform
to transform the value
value
passed to the function.When
value
value
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
useParallax
useParallax
is the scrollYProgress
scrollYProgress
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,
scrollYProgress
scrollYProgress
is0
, thereforeyYellow
yYellow
andyGreen
yGreen
will be-300
and-600
respectively. - When the wrapper div is at the middle of the screen,
scrollYProgress
scrollYProgress
is0.5
, thereforeyYellow
yYellow
andyGreen
yGreen
will both be0
. - When the wrapper div is at the bottom of the screen,
scrollYProgress
scrollYProgress
is1
, thereforeyYellow
yYellow
andyGreen
yGreen
will be300
and600
respectively.
All we have to do now is pass the
yYellow
yYellow
and yGreen
yGreen
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
y
y
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.
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>
);
}