
How to Create a Cool Parallax Effect in React.js
2025-01-27
Parallax scrolling is a popular web design trend that creates a sense of depth by moving background and foreground elements at different speeds. It’s visually engaging and relatively simple to implement in React. In this tutorial, we’ll build a basic parallax effect step-by-step and make it reusable and easy to use.
What You’ll Learn
• How the parallax effect works
• How to implement it using React
• Reusable parallax provider
Prerequisites
• Basic knowledge of React and CSS
• Node.js installed on your system
• A text editor (e.g., VSCode)
1. Set Up Your React Project
If you don’t already have a React project, create one with the following command:
yarn create vite parallax-effect-example --template react-ts
cd parallax-effect-example
yarn install
Then, start the development server:
yarn dev
Then open in the browser:
http://localhost:5173/
2. Install Dependencies
We’ll use the d3-scale library to simplify our implementation. Install it with:
yarn add d3-scale
yarn add -D @types/d3-scale
3. Basic Project Structure
Create the following file structure for the project:
src/
components/
ParallaxSection.tsx
SectionProvider.tsx
Shape.tsx
App.tsx
index.css
4. Create Section Provider
Create a section provider that will pass props to the parallax section. We need to get the window height and section position using useEffect.
const [myPosY, setMyPosY] = useState<number>( 0 )
const [winHeight, setWinHeight] = useState<number>( 0 )
const sectionRef = useRef<HTMLDivElement>( null )
useEffect( () => {
// Add event listener when component mounts
setWinHeight( window.innerHeight )
window.addEventListener( 'scroll', handleScroll )
// Cleanup function to remove event listener
return () => {
window.removeEventListener( 'scroll', handleScroll )
}
}, [] )
function handleScroll() {
if ( sectionRef.current ) {
const { top } = sectionRef.current?.getBoundingClientRect() as DOMRect
setMyPosY( top )
}
}
Next, we need to validate if the Section provider has valid children, and then pass props to the children. This way, the child elements will always receive the myposy and winheight props.
<div ref={sectionRef}>
{React.Children.map( children, ( child ) => {
if ( React.isValidElement( child ) ) {
return React.cloneElement( child, {
myposy : myPosY,
winheight : winHeight,
} ) // Passing myPosY to React child components
}
return child
} )}
</div>
5. Create a Parallax Section Component
We can use the myposy props to create the parallax effect. The parallax effect is achieved by dynamically adjusting the position and opacity of elements based on the vertical scroll position (myposy). Here's a breakdown of how it works, using the d3-scale library to control the transformations:
- Translate Y Movement:
- The translate variable is a scalePow function from the d3-scale library. It maps the vertical scroll position (myposy) to a range of vertical translation values for the elements in the parallax section.
- The domain([-2000, 2000]) defines the input range for the scroll position, and range([-100, 100]) defines the output range for the translation (vertical movement).
- As the scroll position (myposy) changes, the value is passed through this scale function to determine how much the elements should move vertically. For instance, when scrolling down, elements might move upwards, and vice versa, creating the parallax effect.
- Opacity Effect:
- The opacity variable also uses a scalePow scale to control the opacity of the section based on the scroll position. It adjusts from opacity: 1 (fully visible) to opacity: 0 (invisible) as the user scrolls.
- The domain([0, ref.current?.offsetHeight - 40]) defines the scroll position range that will trigger the opacity change. This ensures the opacity fades out as the section is scrolled away.
- The opacity is applied by passing myposy through the opacity.exponent(1) function, adjusting the opacity value as the user scrolls.
- Dynamic Style Updates:
- The transform style properties in the avatar-wrapper and profile-content divs apply the vertical translation (translateY()) as determined by the translate.exponent(1)(myposy ? -myposy : 0) value. This adjusts the position of these elements based on the scroll position.
- The values are calculated and applied dynamically, which causes the background image and profile content to shift vertically at different rates, creating the parallax effect.
import { FunctionComponent, useRef } from 'react'
import { scalePow } from 'd3-scale'
import Shape from './Shape'
interface Props {
myposy?: number
winheight?: number
}
const ParallaxSection: FunctionComponent<Props> = ({ myposy }) => {
const ref = useRef<HTMLElement>(null)
const translate = scalePow().domain([-2000, 2000]).range([-100, 100])
const opacity = scalePow()
.domain([
0,
ref.current?.offsetHeight ? ref.current?.offsetHeight - 40 : 1000,
])
.range([1, 0])
return (
<section
ref={ref}
id="home"
className="main__section"
style={{
opacity: opacity.exponent(1)(myposy ? -myposy : 1),
}}
>
<Shape myposy={myposy || 0} />
<div className="banner-image">
<div
className="avatar-wrapper"
style={{
transform: `translateY(${translate.exponent(1)(
myposy ? -myposy : 0
)}px)`,
}}
>
<div className="avatar"></div>
</div>
</div>
<div
className="profile-content"
style={{
transform: `translateY(${translate.exponent(1)(
myposy ? -myposy : 0
)}px)`,
}}
>
<h1 className="profile-title">This is Parallax</h1>
<div className="profile-bio">
<p>
Lorem Ipsum is simply dummy text of the printing and typesetting
industry. Lorem Ipsum has been the industry's standard dummy text
ever since the 1500s, when an unknown printer took a galley of type
and scrambled it to make a type specimen book. It has survived not
only five centuries, but also the leap into electronic typesetting,
remaining essentially unchanged. It was popularised in the 1960s
with the release of Letraset sheets containing Lorem Ipsum passages,
and more recently with desktop publishing software like Aldus
PageMaker including versions of Lorem Ipsum.
</p>
</div>
</div>
</section>
)
}
export default ParallaxSection
6. Update Your App Component
Import and use the ParallaxSection component in App.js:
import SectionProvider from './components/SectionProvider'
import ParallaxSection from './components/ParallaxSection'
function App() {
return (
<main>
<SectionProvider>
<ParallaxSection />
</SectionProvider>
<section
className="main__section"
style={{
height: '1000px',
backgroundColor: 'var(--dark-secondary)',
}}
></section>
</main>
)
}
export default App
7. Add Styling
Delete your app.css and change all the index.css with code below:
:root {
--dark: #222222;
--dark-secondary: #393939;
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#root {
width: 100vw;
}
body {
margin: 0;
display: flex;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
.main__section {
padding: 0 !important;
background-color: var(--dark);
position: relative;
margin-inline: auto;
overflow: hidden;
}
.banner-image {
display: flex;
width: 100%;
height: 14rem;
position: relative;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-color: rgb(159, 159, 159);
}
.avatar-wrapper {
position: absolute;
bottom: -6rem;
left: 0px;
right: 0px;
margin-inline: auto;
width: 12rem;
aspect-ratio: 1;
border: none;
border-radius: 50%;
overflow: hidden;
}
.avatar {
background-color: var(--dark-secondary);
width: 100%;
height: 100%;
}
.profile-content {
width: 100%;
max-width: 48rem;
margin: 8rem auto 5rem;
display: flex;
flex-direction: column;
gap: 1rem;
padding: 0 1rem;
overflow: hidden;
}
.profile-title {
text-align: center;
margin: 0;
}
.profile-bio {
text-align: center;
color: rgba(255, 255, 255, 0.75);
}
.social-links .dot {
display: inline-block;
}
.last-hidden:last-child {
display: none;
}
/* Shape */
.shape {
position: absolute;
}
.shape-1 {
width: 40px;
height: 40px;
background-color: var(--dark-secondary);
top: 80%;
left: 6rem;
}
.shape-2 {
width: 0;
height: 0;
top: 10%;
left: 12rem;
border-left: 20px solid transparent;
border-right: 20px solid transparent;
border-bottom: 30px solid var(--dark-secondary);
}
.shape-3 {
width: 0;
height: 0;
top: 25%;
left: 75%;
border-left: 20px solid transparent;
border-right: 20px solid transparent;
border-bottom: 30px solid var(--dark-secondary);
}
.shape-4 {
width: 40px;
height: 40px;
background-color: var(--dark-secondary);
top: 66%;
left: 50%;
}
.shape-5 {
width: 40px;
height: 40px;
background-color: var(--dark-secondary);
top: 85%;
left: 90%;
}
.shape-6 {
width: 0;
height: 0;
top: 60%;
left: 50%;
border-left: 20px solid transparent;
border-right: 20px solid transparent;
border-bottom: 30px solid var(--dark-secondary);
}
8. Test the Parallax Effect
Run your app with:
yarn dev
Scroll through the page, and you’ll see the heading and paragraph move at different speeds, creating a parallax effect.
8. Customize and Enhance
Here are some ideas to enhance the effect:
• Add images: Replace text with parallax-enabled images.
• Multiple sections: Add more Parallax components with varying speeds.
• Performance optimization: Use lazy loading for images and limit the number of layers.
Live Demo
Here’s a live demo on CodeSandbox: Parallax Effect in React
Full Code
You can fork this repo to start experimenting with the parallax effect! On GitHub
Conclusion
You’ve successfully implemented a parallax effect in React! This technique can make your website more dynamic and engaging. Experiment with different speeds and elements to create unique designs.