Background Image Parallax

How to Make a Background Image Parallax using Framer Motion and React

NarakCODEOctober 19, 2024

Scroll
Parallax
Framer Motion
Nextjs
image

Creating the Project

Let's start the project by creating a Next.js application. We can do that by running npx create-next-app@latest client inside of a terminal.

  • We will use Framer Motion for the animation, so we can run npm i framer-motion.
  • We will use the Lenis Scroll for the smooth scrolling, so we can run npm i lenis.

Basic Background Parallax

The basics of creating a background image parallax is as follow:

  • Create a container with an overflow hidden
  • Add an image inside of that container taking the full width and height of it
  • Translate the image on scroll
1// components/Intro.jsx
2import React from 'react';
3import Image from 'next/image';
4import Background from '../../public/images/2.jpg';
5import { useScroll, useTransform, motion } from 'framer-motion';
6import { useRef } from 'react';
7
8export default function Intro() {
9  const container = useRef();
10  const { scrollYProgress } = useScroll({
11    target: container,
12    offset: ['start start', 'end start'],
13  });
14
15  const y = useTransform(scrollYProgress, [0, 1], ['0vh', '150vh']);
16
17  return (
18    <div className='h-screen overflow-hidden'>
19      <motion.div style={{ y }} className='relative h-full'>
20        <Image
21          src={Background}
22          fill
23          alt='image'
24          style={{ objectFit: 'cover' }}
25        />
26      </motion.div>
27    </div>
28  );
29}

Couple notes about the code above:

  • The progress of the scroll is tracked using the useScroll Hook
  • Since the component is the first inside the page, we use an offset of ['start start']
  • The progress of the scroll (a value between 0 and 1) is transformed into a value between 0vh and 150vh and used as a translateY value

Then I'm adding that component inside the page.js:

1// page.js
2'use client';
3import { useEffect } from 'react';
4import Lenis from 'lenis';
5import Intro from '@/components/Intro';
6import Description from '@/components/Description';
7
8export default function Home() {
9  useEffect(() => {
10    const lenis = new Lenis();
11
12    function raf(time) {
13      lenis.raf(time);
14      requestAnimationFrame(raf);
15    }
16
17    requestAnimationFrame(raf);
18  }, []);
19
20  return (
21    <main>
22      <Intro />
23      <Description />
24      <div className='h-screen'></div>
25    </main>
26  );
27}
1// components/Description.jsx
2import React from 'react';
3
4export default function Description() {
5  return (
6    <div className='flex justify-center my-40'>
7      <p className='text-[7.5vw] uppercase text-center max-w-[50vw] leading-none'>
8        The quick brown fox jumps over the lazy dog
9      </p>
10    </div>
11  );
12}

We should have something like this:

Advanced Background Parallax

The other way of making a background parallax is the same way but instead the container will be in position fixed.

1// components/Section.jsx
2import Image from 'next/image';
3import Background from '../../public/images/1.jpg';
4import Text from './components/Text';
5import { useScroll, useTransform, motion } from 'framer-motion';
6import { useRef } from 'react';
7
8export default function Section() {
9  const container = useRef();
10  const { scrollYProgress } = useScroll({
11    target: container,
12    offset: ['start end', 'end start'],
13  });
14  const y = useTransform(scrollYProgress, [0, 1], ['-10vh', '10vh']);
15
16  return (
17    <div
18      ref={container}
19      className='relative flex items-center justify-center h-screen overflow-hidden'
20      style={{ clipPath: 'polygon(0% 0, 100% 0%, 100% 100%, 0 100%)' }}
21    >
22      <Text />
23      <div className='fixed top-[-10vh] left-0 h-[120vh] w-full'>
24        <motion.div style={{ y }} className='relative w-full h-full'>
25          <Image
26            src={Background}
27            fill
28            alt='image'
29            style={{ objectFit: 'cover' }}
30          />
31        </motion.div>
32      </div>
33    </div>
34  );
35}
  • Couple notes about the above code:
    • The image container is in position fixed, so we need to add a clip-path on the parent to crop that fixed div.
    • The same principles apply from the previous implementation, we translate the image based on the position of the scroll.
    • The amount translated here (-10vh and 10vh) should be accounted in the height of the fixed container (120vh).

We should have something like this:

Wrapping up

That's it for this animation!

A super clean animation that in my opinion should be in almost every single websites, it really adds a nice dimensionality and brins it to another level of quality. Hope you learned something!