Before starting out, let me quickly share what we're building today.
Open codesandbox.io or visit gsap-svg-path-animation.netlify.app.
Seems interesting, let's start.
Introduction
You might have visited some websites where when you scroll, things move; some elements move up, some sideways, and sometimes move down also. The elements follow a path when they move and often the path they follow is drawn using <path> element in SVG.
Brief overview of SVG animation
Unlike raster images, the SVGs don't lose their quality on zooming because SVGs are just a bunch of coordinates to make mathematical polygons. Also, unlike raster images, SVGs can be animated. We can change color, change size, change shape, and do more cooler stuff. That's all about the SVG animation.
Now let me explain what an SVG path animation means. So, if you've worked with SVGs before, there are different shapes that you can draw. There are ellipses, lines, polygons, rectangles, circles, and many others. You can find the complete list here. In particular, there is something called path
that is used to draw lines, irregular lines; as you do with your hands. Unlike hand-drawn lines, the lines drawn with path elements can be animated. The path is used to guide some elements to move in a specific direction, basically a path and sometimes used to animate the line itself.
Introduction to Next.js
I assume you know Next.js. If you don't that's ok. You'll get almost everything out of this article. So, for those who don't know what Next.js is, let me quickly share about the most loved framework by lazy developers (I don't know if it's most loved or not; I love it).
Next.js is a javascript framework, like React.js but better. Next. js supports Server-Side Rendering (SSR), whereas React. js supports client-side rendering. Doesn't sound like a big deal but it is. SSR improves the application performance and speed and that's the reason I love Next.js.
Introduction to GSAP (GreenSock Animation Platform)
Unlike Next.js, I don't assume you know GSAP. So, GSAP which stands for GreenSock Animation Platform is another javascript framework (yes, there are millions of javascript frameworks) that is used to animate elements on websites. Open their website (gsap.com) to explore the type of animations you can make using GSAP.
Purpose and scope of the article
The article's purpose is to help you make GSAP animations in your Next.js project. It's the same for React also. In particular, we'll be looking at SVG path animation on scroll. I assume you saw the demo above.
Setting Up the Development Environment
Now let's move on to the actually stop so we are going to set up our development environment and we will be using the npm package manager and create our next application, and use the GSAP library.
Installing Node.js and npm
I assume you already have nodejs installed. If you don't then head over to not nodejs.org and download the latest version. Remember I want you to download the long-term support (LTS) version. If you're on Linux then this:
or this: https://dev.to/aashishpanthi/3-easy-ways-to-install-nodejs-in-linux-lc4 article will help you.
To confirm you've installed node.js, run this command on your command prompt or terminal:
node --version
And you should be seeing some numbers on the screen that represent the version of node.js installed on your machine. It looks like this:
Now that you have installed the node.js, you need to take for the npm. In most of the cases, the npm gets installed automatically but if it does not, you can install it manually. So let's check if the npm is installed or not. Run the following command on your terminal:
npm --version
And if the npm is installed then, you'll see the version like this:
The basic setup is complete. Now let's move on to nitty-gritty.
Setting up a new Next.js project
If you would love to read the official documentation on installing nextjs then here it is. Otherwise, I'm here to help.
Now, to install next.js we need to run this command on our terminal:
npx create-next-app@latest
Now, it'll ask a few questions. These questions are important and carefully answer them.
First, you'll be asked to name the project. Please name it.
You'll be asked if you want to use typescript or not. I prefer typescript but it's your call. (In the rest of the article, we'll be using typescript but I'll provide the javascript code at the end).
I like to use Eslint.
Also, I'll be using tailwind CSS. If you want to sit and write your custom CSS then you've a right to opt out.
Next, it will ask if you'd like to have a
src
directory or not. It's more of a personal preference and I would say yes here.And please use the app router. Please...
The final question asks you about the default import alias. We don't want to do that so select no.
So, after a few seconds, your project will be ready and I would love if you open the project on some code editor. I'll be using Visual Studio Code.
Go inside of the project and open the terminal. Run the start command to start the project:
npm run dev
And visit http://localhost:3000/ to see your project running.
Installing GSAP
Let's quickly install the GSAP packages also. Use the following command to install gsap
and @gsap/react
packages:
npm i @gsap/react gsap
That's all. First, let's have our SVG ready and then we'll animate. Alright?
Creating SVG Graphics
I assume you already have your SVG element ready. If you have it on your figma, export it as SVG. If you don't have you can make one.
Designing SVG graphics
I quickly made this shape in Figma using pen tool. It is basically the path over which we'll be moving stuff on the scroll event. I still don't know what I'll be moving.
To export your SVG from Figma, select the shape you want to export. If you have more than one shape, select the shapes and group them then export them as SVG. You'll see the export option at the right end of the screen:
Well, if you would like to use mine then here is the SVG code:
<svg width="462" height="829" viewBox="0 0 462 829" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M36.4996 4L429.5 238C449.666 250 479 284.9 435 328.5C391 372.1 151 480.333 36.4996 529C12.1662 545.167 -21.9004 587.7 36.4996 628.5C94.8996 669.3 326.5 776.167 435 824.5" stroke="#0000FF" stroke-width="8"/>
</svg>
Ok, that's the path and let's move a card along that path. Sound great? I've made a car. I'm not a designer. Please don't judge me:
I've grouped the two items and exported them as SVG. If you want the code, here it is:
<svg width="462" height="844" viewBox="0 0 462 844" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M36.4996 18.9999L429.5 253C449.666 265 479 299.9 435 343.5C391 387.1 151 495.333 36.4996 544C12.1662 560.167 -21.9004 602.7 36.4996 643.5C94.8996 684.3 326.5 791.167 435 839.5" stroke="#0000FF" stroke-width="8"/>
<path d="M37.7316 37.3941L46.4471 37.0985C46.7214 37.0872 46.9392 36.9896 47.1001 36.8317L60.2671 45.3825C62.7497 46.9947 65.6719 46.8955 66.7971 45.163L73.9314 34.1771C75.0579 32.4425 73.9598 29.7326 71.4773 28.1204L58.3102 19.5696C58.3877 19.3605 58.3896 19.1198 58.2883 18.8645L55.0122 10.7844C54.706 10.0288 53.6614 9.44663 52.673 9.47815C51.6862 9.51362 51.134 10.152 51.4373 10.9057L53.7501 16.6082L41.4135 8.59676C38.9339 6.98646 36.0087 7.08368 34.8822 8.81829L27.7479 19.8042C26.6228 21.5367 27.7237 24.2485 30.2034 25.8588L42.5399 33.8703L36.387 34.081C35.5765 34.1085 35.2193 34.8705 35.5882 35.7864C35.9614 36.7022 36.9182 37.4197 37.7316 37.3941ZM56.9649 23.0482L56.3711 23.9626L46.2266 17.3747L46.8204 16.4603L56.9649 23.0482ZM40.0447 12.0601L43.6442 15.6977L37.5977 25.0085L32.8092 23.2018L40.0447 12.0601ZM39.5849 27.602L40.1801 26.6855L50.3246 33.2734L49.7294 34.1899L39.5849 27.602ZM52.8688 34.9256L58.9153 25.6149L63.7025 27.4236L56.467 38.5653L52.8688 34.9256Z" fill="#E52A2D"/>
</svg>
Importing SVG files into the Next.js project
No, we are not going to import the SVG file. Rather we will have SVG inside our JSX file with other react neety-greety.
Let's create a separate component for the Animation thing and we'll import it inside of our page.tsx file. And the root page.tsx
file should look like this:
import PathAnimation from "@/components/PathAnimation";
export default function Home() {
return (
<main className="flex-col min-h-screen w-full dark:text-white">
<div className="h-screen w-full flex flex-col items-center justify-center">
<h1 className="text-[#432826] text-[24px] leading-[36px] font-[400] text-center dark:text-white">
SVG Path Animation
</h1>
<p className="text-[16px] leading-[27px] text-[#432826] font-[400] text-center dark:text-white">
This is a simple animation of a bee following a path using GSAP
MotionPath plugin.
</p>
</div>
<PathAnimation />
<div className="h-screen w-full flex flex-col items-center justify-center">
<h1 className="text-[#432826] text-[24px] leading-[36px] font-[400] dark:text-white">
You saw that?
</h1>
<p className="text-[16px] leading-[27px] text-[#432826] font-[400] dark:text-white">
The bee is following the path. Isn't that cool?
</p>
</div>
</main>
);
}
And let's have our PathAnimation.jsx
file separately (create a components folder and place it there). The component looks like this:
function PathAnimation() {
return (
<section
className="py-16 px-4 md:px-2 bg-secondary w-full"
>
<div className={`max-w-[1250px] w-full mx-auto relative mt-16 mb-20`}>
<svg
width="462"
height="844"
viewBox="0 0 462 844"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="w-full h-full"
>
<path
d="M36.4996 18.9999L429.5 253C449.666 265 479 299.9 435 343.5C391 387.1 151 495.333 36.4996 544C12.1662 560.167 -21.9004 602.7 36.4996 643.5C94.8996 684.3 326.5 791.167 435 839.5"
stroke="#0000FF"
strokeWidth="8"
/>
<path
d="M37.7316 37.3941L46.4471 37.0985C46.7214 37.0872 46.9392 36.9896 47.1001 36.8317L60.2671 45.3825C62.7497 46.9947 65.6719 46.8955 66.7971 45.163L73.9314 34.1771C75.0579 32.4425 73.9598 29.7326 71.4773 28.1204L58.3102 19.5696C58.3877 19.3605 58.3896 19.1198 58.2883 18.8645L55.0122 10.7844C54.706 10.0288 53.6614 9.44663 52.673 9.47815C51.6862 9.51362 51.134 10.152 51.4373 10.9057L53.7501 16.6082L41.4135 8.59676C38.9339 6.98646 36.0087 7.08368 34.8822 8.81829L27.7479 19.8042C26.6228 21.5367 27.7237 24.2485 30.2034 25.8588L42.5399 33.8703L36.387 34.081C35.5765 34.1085 35.2193 34.8705 35.5882 35.7864C35.9614 36.7022 36.9182 37.4197 37.7316 37.3941ZM56.9649 23.0482L56.3711 23.9626L46.2266 17.3747L46.8204 16.4603L56.9649 23.0482ZM40.0447 12.0601L43.6442 15.6977L37.5977 25.0085L32.8092 23.2018L40.0447 12.0601ZM39.5849 27.602L40.1801 26.6855L50.3246 33.2734L49.7294 34.1899L39.5849 27.602ZM52.8688 34.9256L58.9153 25.6149L63.7025 27.4236L56.467 38.5653L52.8688 34.9256Z"
fill="#E52A2D"
/>
</svg>
</div>
</section>
);
}
export default PathAnimation;
Now, if you save and look at the browser, you should be able to see the SVG covering up the whole screen.
Integrating GSAP with Next.js
Oh the juicy part, let's start with the animation. We got to move that car along the path, right? Let's do that.
Importing GSAP into Next.js components
First we need to import the gsap and then import the useGSAP hook. The useGSAP hook is fairly new release and you don't find it around the internet (as of early 2024).
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
After that, we need to import the plugins and register those plugins:
import MotionPathHelper from "gsap/MotionPathPlugin";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(useGSAP, ScrollTrigger);
gsap.registerPlugin(MotionPathHelper);
That's all about importing and registering stuffs related to GSAP. Now, let's move on to the next part.
Using useRef to handle events
You know in react and next.js, we cannot directly manipulate the elements. So, we need to use useRef hook. We're going to have four refs:
const lineRef = useRef<SVGPathElement>(null);
const carRef = useRef<SVGPathElement>(null);
const container = useRef<SVGSVGElement>(null);
const sectionRef = useRef<HTMLDivElement>(null);
As you might have already guessed where you need to place these refs. Well, here it is:
return (
<section
className="py-16 px-4 md:px-2 bg-secondary w-full"
ref={sectionRef}
>
<div className={`max-w-[1250px] w-full mx-auto relative mt-16 mb-20`}>
<svg
width="462"
height="844"
viewBox="0 0 462 844"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="w-full h-full overflow-visible"
ref={container}
>
<path
ref={lineRef}
d="M36.4996 18.9999L429.5 253C449.666 265 479 299.9 435 343.5C391 387.1 151 495.333 36.4996 544C12.1662 560.167 -21.9004 602.7 36.4996 643.5C94.8996 684.3 326.5 791.167 435 839.5"
stroke="#0000FF"
strokeWidth="8"
/>
<path
ref={carRef}
d="M45.5126 32.8551L52.6609 27.8605C52.8849 27.7015 53.0143 27.5011 53.0633 27.281L68.7632 27.281C71.7233 27.281 74.1201 25.6064 74.1201 23.5405V10.4414C74.1201 8.37307 71.7233 6.69838 68.7632 6.69838L53.0633 6.69838C53.0143 6.4808 52.8849 6.27788 52.6609 6.11897L45.5126 1.12674C44.8442 0.659786 43.6511 0.740459 42.8393 1.3052C42.0311 1.87239 41.9156 2.70852 42.5804 3.17547L47.6259 6.69838L32.9163 6.69838C29.9596 6.69838 27.5593 8.37307 27.5593 10.4414L27.5593 23.5405C27.5593 25.6064 29.9596 27.281 32.9163 27.281L47.6259 27.281L42.5804 30.8088C41.9156 31.2734 42.0311 32.107 42.8393 32.6742C43.6511 33.239 44.8442 33.3196 45.5126 32.8551ZM53.8296 10.3485V11.4388L41.7337 11.4388V10.3485L53.8296 10.3485ZM33.6545 10.3485L38.6546 11.4388L38.6546 22.5406L33.6545 23.6334L33.6545 10.3485ZM41.7337 23.6334V22.5406L53.8296 22.5406L53.8296 23.6334L41.7337 23.6334ZM56.8632 22.5406V11.4388L61.8632 10.3485L61.8632 23.6334L56.8632 22.5406Z"
fill="#E52A2D"
/>
</svg>
</div>
</section>
);
The svg code for car has been changed. Basically, I rotated the card. That's all.
So, after changing the path element, import the useEffect hook from react and we are going to set the position of the car.
useEffect(() => {
gsap.set(carRef.current, {
yPercent: 0,
xPercent: 20,
rotate: 30,
});
}, []);
The above code positions the car at y=0%
, x=20%
and rotation=30deg
(property names aren't exactly the same, it's used to just explain)
So, our path is there, car is there. And our code looks like this:
"use client";
import { useEffect, useRef } from "react";
//gsap
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
import MotionPathHelper from "gsap/MotionPathPlugin";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(useGSAP, ScrollTrigger);
gsap.registerPlugin(MotionPathHelper);
function PathAnimation() {
const lineRef = useRef<SVGPathElement>(null);
const carRef = useRef<SVGPathElement>(null);
const container = useRef<SVGSVGElement>(null);
const sectionRef = useRef<HTMLDivElement>(null);
useEffect(() => {
gsap.set(carRef.current, {
yPercent: 0,
xPercent: 20,
rotate: 30,
});
}, []);
return (
<section
className="py-16 px-4 md:px-2 bg-secondary w-full"
ref={sectionRef}
>
<div className={`max-w-[1250px] w-full mx-auto relative mt-16 mb-20`}>
<svg
width="462"
height="844"
viewBox="0 0 462 844"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="w-full h-full overflow-visible"
ref={container}
>
<path
ref={lineRef}
d="M36.4996 18.9999L429.5 253C449.666 265 479 299.9 435 343.5C391 387.1 151 495.333 36.4996 544C12.1662 560.167 -21.9004 602.7 36.4996 643.5C94.8996 684.3 326.5 791.167 435 839.5"
stroke="#0000FF"
strokeWidth="8"
/>
<path
ref={carRef}
d="M45.5126 32.8551L52.6609 27.8605C52.8849 27.7015 53.0143 27.5011 53.0633 27.281L68.7632 27.281C71.7233 27.281 74.1201 25.6064 74.1201 23.5405V10.4414C74.1201 8.37307 71.7233 6.69838 68.7632 6.69838L53.0633 6.69838C53.0143 6.4808 52.8849 6.27788 52.6609 6.11897L45.5126 1.12674C44.8442 0.659786 43.6511 0.740459 42.8393 1.3052C42.0311 1.87239 41.9156 2.70852 42.5804 3.17547L47.6259 6.69838L32.9163 6.69838C29.9596 6.69838 27.5593 8.37307 27.5593 10.4414L27.5593 23.5405C27.5593 25.6064 29.9596 27.281 32.9163 27.281L47.6259 27.281L42.5804 30.8088C41.9156 31.2734 42.0311 32.107 42.8393 32.6742C43.6511 33.239 44.8442 33.3196 45.5126 32.8551ZM53.8296 10.3485V11.4388L41.7337 11.4388V10.3485L53.8296 10.3485ZM33.6545 10.3485L38.6546 11.4388L38.6546 22.5406L33.6545 23.6334L33.6545 10.3485ZM41.7337 23.6334V22.5406L53.8296 22.5406L53.8296 23.6334L41.7337 23.6334ZM56.8632 22.5406V11.4388L61.8632 10.3485L61.8632 23.6334L56.8632 22.5406Z"
fill="#E52A2D"
/>
</svg>
</div>
</section>
);
}
export default PathAnimation;
Now, the only thing remaining is the ignition of the engine and letting the car move.
Creating animation timelines
It's time to use useGSAP
hook provided by gsap to create an animation timeline.
useGSAP(
(context, contextSafe) => {
let tl = gsap.timeline({});
tl.to(carRef.current, {
motionPath: {
path: lineRef.current || "",
align: lineRef.current || "",
},
ease: "power1.inOut",
});
return tl;
},
{
scope: container,
}
);
The above timeline is a basic timeline. If you want to dive deep into the useGSAP
hook then I recommend this official documentation. Allow me to simplify further.
First, we are creating a timeline with gsap.timeline({})
and we're passing empty object because we don't want to pass anything right now (we'll do in a few seconds). Then the timeline is stored in tl
variable, tl is short for the timeline. We're storing the timeline on a variable to easily access the timeline in the future.
Then, we're passing the actual props to animate the stuff. The animation is targeted to the car path, we're using carRef
to point to our car. Then, we're proving motionPath, inside of the motionPath, we can provide the path (along which the car moves) and another path (along which the car aligns).
At the end, we're providing the scope also. It's helpful when you've multiple animations and you don't want to mess up.
Now, let's make a final timeline.
useGSAP(
(context, contextSafe) => {
let tl = gsap.timeline({
scrollTrigger: {
trigger: sectionRef.current,
scrub: true,
start: "top center",
end: "bottom center",
},
});
tl.to(carRef.current, {
motionPath: {
path: lineRef.current || "",
align: lineRef.current || "",
alignOrigin: [0.2, 0.5],
autoRotate: true,
start: 0,
end: 1,
},
ease: "power1.inOut",
});
return tl;
},
{
scope: container,
}
);
Here, we're triggering the animation on scroll with scrollTrigger
. Inside scrollTrigger, we're passing a trigger prop; trigger prop is used to identify by with element the animation is going to trigger. Here we're triggering or starting the animation when the section is scrolled halfway to the screen from the top and stops when the bottom end of the section goes halfway through the screen. scrub
is set true to allow animation to take place both forward and backward.
Next, we're aligning the car to perfectly center in that path and also allowing the car to autoRotate
so that the car makes a smooth turn. And, you can add a lot of transitions. Choose the one that suits you best.
Conclusion
In this article, you learned how to create SVG path animations using GSAP in a Next.js project. We covered setting up the development environment, installing necessary packages, creating and importing SVG graphics, and integrating GSAP for smooth animations triggered by scroll events. By the end, you have a car following a path on your webpage, showcasing the power of SVG and GSAP animations.
I would like to end this article with a few resources:
GitHub repository -> https://github.com/aashishpanthi/svg-path-animation
CodeSandBox -> https://codesandbox.io/p/github/aashishpanthi/svg-path-animation/main