Using Contentful, SwiperJS, and GSAP Together: Technologies Overview

For this demo project we will be using four technologies

  1. NextJSThe React Framework for Production – “Next.js gives you the best developer experience with all the features you need for production: hybrid static & server rendering”
  2. Contentful CMS – It’s the easiest, fastest way to manage content: Integrate your tools. Publish across channels. Unblock your team with our industry-leading app framework.
  3. SwiperJS –  Swiper is the most modern free mobile touch slider with hardware accelerated transitions and amazing native behavior.
  4. GSAP – Professional-grade JavaScript animation for the modern web

Contentful is where we will be getting our data for the SwiperJS carousel.

Using Contentful, SwiperJS and GSAP Together: Project Setup

Our first order of business is to create our NextJS app. We can do this really quickly using their automatic setup commands.

If you’re looking to create the app using javascript you can use the following commands:

npx create-next-app@latest
// or
yarn create next-app

However, if you’re interested in using Typescript, which is what we will be using in this demo, you can add the ‘–typescript’ flag to the end of the commands as follows:

npx create-next-app@latest --typescript
// or
yarn create next-app --typescript

Note that you do not need to use typescript to follow this demo.

Using Contentful, SwiperJS and GSAP Together: Setup Contentful

Now that our project is set up, we can step away for a second and get Contentful up and running to supply our data for the SwiperJS carousel. Head over to contentful.com and sign up for a free account, or login if you already have an account.

Initially you will be on the Free Community Tier which gives you one free Contentful Space, which is fine for this demo project. Our first step in our space will be to create our Content Models. Our content model is a collection of Content Types which are like schemas that represent different data and the first one we will be making is the Slide. However, before we get into creating our content models, let’s first add the images we will use as frames for our GSAP animation.

Add Animation Frame Images

Assuming we already have the images we want to use in the animation, add the image to contentful space as “Assets”. This will let us use these images in our content types, which we will do later in a content type called “Animation”.

It would be very helpful to have the frames labeled by their order number so you can correctly tell which frames go before and after each other. This will help in a later step.

To do this, we want to navigate to the “Media” tab in Contentful. There should be a blue button labeled “Add Asset”. When we click this, we should get a dropdown that asks us if we want to upload a single asset or multiple assets. Since we are uploading frames of an animation, we want to choose multiple assets. Note, if your frame count exceeds 20, you will have to do this more than once due to contentful limiting the amount of assets we can upload at once to 20.

Upon clicking multiple assets, just follow the modal that opens up and select the images you want to upload. Also note that when your media tab is populated, the “Add Assets” button moves to the top right of the page.

Once we have the images uploaded, Contenful doesn’t automatically publish them, so we will need to publish all our newly added images. You can do this quickly by clicking the select all checkbox and clicking publish from the list of options that appear. Note the “select all” checkbox is not labeled, but it is the checkbox that is next to the word “preview” in the heading row of the table. Also, if the amount of images you uploaded overflowed into a second page for the table, you will have to repeat this publishing step for that page and any other pages that follow.

Now that we have all our frames uploaded and published, we can go ahead and start making our content types.

Add Slide Content Type

Before we get into creating our content types, a good practice for every content type is to add a field called Internal Name. This field won’t ever be used in code, but will be good for identifying each instance of a content type. To do this, just create a new Text field identical to our “Content” field. Once you create the field, take a quick look to make sure the Internal Name is labeled with “Entry title”. If it isn’t, click the three dots and select “Set field as Entry title”.

To get started, we want to navigate to the Content Model tab in our space and click the “Design your content model” button. This will prompt us to create our first content type and open a model asking for some information about our content type. For the name, we will enter Slide and if you notice the API identifier is automatically generated based on the name we enter. The API identifier is editable but it’s best to just leave it up to what Contentful generates. We can also add a description to describe what the content type is but for this demo we don’t need one so we can just go ahead and click the “Create” button.

You should now be prompted to add a field. Fields are the different pieces of information about the content type. In the case of our slide, we only have one field that we will need, and we’ll call it “Content”. Before we can do anything with the field, however, we are prompted to choose the field type. Contentful gives many options for field types from normal text fields, to dates, booleans and even references to other content types. We will use references in our next content type, but for now we will just be making a Text field. Once we select Text we need to give this field a name which in this case will be Content. Contentful gives a lot of options to customize our fields, such as the type for Text fields. We are given the “Short Text” type and the “Long Text” type which changes features such as character limits. Contentful even lists examples of when each type should be used.

For our case, we can keep Short text selected and click the “Create” button. Now we can click save and we’ve officially created our first content type! Next we will make our Slider content type.

Add Slider Content Type

Our slider content type will hold a collection of Slides. To do this we will utilize the Reference field type. So let’s head back to the Content Model tab and click the “Add content type” button in the top right. We are once again given a model asking us to name the content type so we will enter Slide then click create. Next we will add a field to this content type. When choosing the type of field we want to select “Reference”. Here we will once again be prompted for a field name, in which case we can simply put Slides, however, an important option to look at is the “Select type” section. Here we define whether or not we should be able to add multiple references. Since we want the ability to add multiple slides, we will select the “Many references” option.

Now, instead of clicking create, we want to click the “Create and configure” button. This will open another model with more advanced options we can change about the field. The main option we need to change is under the “Validation” tab. There is an option that is labeled “Accept only specified entry type” which we want to toggle on. Once we toggle it on, we are shown a list of all the content types we have created, which in our case is only Slide and Slider. We only want to allow Slides in our slider so we will select “Slide” and click the “Confirm” button.

Add Animation Content Type

Our “Animation” content type will hold all our animation frames we uploaded. Let’s start by heading back to the Content Model tab, and click “Add content type”. We want to name this content type “Animation” and add our “Internal Name” field like we did in all the previous content types. The next field we want to add is a “Media” field. When the modal prompts you for the name of this media field, which I named “Frames”, pay attention to the “Select type” section of the modal. Here it asks if we want to be able to upload one or multiple media files. Since we have many frames to an animation, we will want to select the “many files” option. Then we can click create. With that our animation content type is completed.

Creating Entries

Now that we’ve made our Content Types, we can now use them to create entries which are instances of the content types. Firstly, we’re going to make 3 Slide entries.

Navigate to the Content tab and select the “Add entry” button in the center of the page. This will open a dropdown with our two Content Types. We are creating a slide entry so select Slide. A new page should be opened with the “content” field present and empty. This is our new Slide entry, however, it is currently empty and in a “draft” state. Now I won’t go too deep into contentful’s different states and API’s, but basically, we can alternate entries through a draft and published state to let the Delivery API know if it should be included or not.

In our “content” field we will enter a message we want to be displayed on this slide. This message can be anything you want, in my case I am just putting “Slide 1”. I will follow this naming convention for the other two Slides I will make. Once this is done, select publish to publish the entry.

In total we want to have 3 Slide entries with unique “content” field values.

Next up is creating our Slider entry. Navigating back to the Content tab, select the Add Entry button in the top right and select Slider. We first want to enter the Internal Name which we will call “Main Slider ”. Next we want to add the three slides we created. To do this, under the Slides field select the “Add content” dropdown and click the “Add existing content” option. A model should appear displaying the three slides we created. Select all three and click the insert 3 entries button. Once this is done select publish to publish this entry.

Lasty, let’s create our animation entry. Head back to the Content tab and select “Add entry” once again. From here, select our Animation content type from the dropdown. We can give it any internal name. To upload the frames we want to click the “Add media” button and click “Add existing media”. Now we can select all the images we uploaded as assets earlier. Note that these may not be added in the correct frame order so you will want to go through and make sure they are in ascending order from top to bottom so the animation plays as it should. The order they are in here is the order we will get them in our project. Once we have our frames added and ordered properly, we can go ahead and publish this entry.

Using Contentful, SwiperJS and GSAP Together: Retrieve Entries from Contentful

Now that we have our data in contentful, we need to get that data into our NextJS project. To do this we will use their Content Delivery API and the contentful npm package. To install the contentful package, simply run

npm i contentful

Next, in our project’s root folder, we will create a new folder called functions and a new file called contentful.ts (or .js if you are not using typescript). Here is where we will store our function that fetches the entry we want from contentful. However, before we can create the function there are 3 pieces of data we need from contentful. Our space id, our environment id and our access token. The environment id will simply have a value of master, but to get our space id and access token we will need to head back into contentful and select the Settings tab. This should toggle a dropdown. In that dropdown we want to select API Keys. You should be taken to a new page that says “Content delivery/preview tokens” with one table item. Select that table item to view the different API keys. From there you can copy your “Space ID” and “Content Delivery API – access token”. Save all three of these pieces of data as variables inside the contentful file we created.

const space = "your_space_id";
const environmentId = "master";
const accessToken = "your_access_token";

Next we want to create a “client” variable that we will later update, then we want to create a conditional that checks to make sure we have an accessToken. This is to make sure we aren’t making requests to the API without one and crashing our project. If we do have an accessToken, which we should, we will populate our client variable with a new contentful client instance using our spaceId, environmentId and accessToken.

if (accessToken){
   client = require('contentful').createClient({
       space,
       accessToken,
       ...(environmentID && { environment: environmentID }),
   })
}

Now that we should have a client instance, we want to create a function that can fetch an entry from contentful. We first create an async function called fetchEntry with a parameter of entryId. We once again want a conditional to make sure we have an accessToken. If we do have an accessToken, we create a variable called entry that will hold the data returned from one of contentful’s functions called getEntry. This getEntry function takes in the entryId and returns all the data about the entry. If we don’t have an accessToken, we can just log something to the console telling us that there is no accessToken.

// The ": string" is only for typescript
export async function fetchEntry(entryId: string) {
   if (accessToken) {
       const entry = await client.getEntry(entryId);
       if (entry) return entry;
       console.error(`Error getting entry with ID ${entryId}`);
   }
   console.error("Access token is undefined");
}

Note this function only gets one entry of a specific content type. If we wanted all entries of a content type or maybe some entries based on filters, we would need to make another function, however, that is outside the scope of this project so I won’t be making a function like that. However, if that’s something you’re interested in, checkout Contentful’s Content Delivery API documentation for more information.

Now that we have our function to fetch our entry, we need a way to pass this to our home page, which is our index file in our pages folder. To do this, we will be using NextJS’ getStaticProps function. We can start by removing all the JSX from the return of our Home component and replacing it with the following div:

<div>Hello World</div>

Next, at the end of our index file we will export a function called getStaticProps. If you are using typescript, NextJS has a type for this function that you can use.

In this function we will fetch all our data from contentful using the fetchEntry function we made. Once we get this data, we will return it inside a props object.

export const getStaticProps: GetStaticProps = async () => {
   // Get the slider and the animation frames
   const slider = await fetchEntry("your_slider_entry_id");
   const animation = await fetchEntry("your_animation_entry_id");
   return {
       props: {
           slider,
           animation,
       },
   };
};

To find the entry id for your entries, go to contentful, click the specific entry you want the ID for, then in the right panel where you would normally change the status of the entry, click info. There it will list your entry id and you can just copy and paste it.

In order to use this data inside our Home component, we pass it in as parameters. You do this with the normal props parameter, or you can destructure it. Below are examples of both ways.

With “props” parameter

const Home = (props) => {
   const {
       slider,
       animation
   } = props;

   return (
       // Component JSX goes here
   )
}

With destructuring

const Home = ({ slider, animation }) => {
   return (
       // Component JSX goes here
   )
}

Note if you are using typescript you will want to give this destructured object a type to avoid errors. Initially you won’t really know what specifically you’re getting back, however, a way to write out types/interfaces for these objects is to console log them and create custom types/interfaces based on the objects you’re getting for slider and animation. Below is what I used during this project. You don’t have to add every aspect of the object, just the data that we need to use so the metadata and sys properties don’t need to be added in for this project. Feel free to just use the types I did but I highly recommend looking through the objects and seeing what exactly contentful is sending you.

interface HomeProps {
   slider: {
       fields: {
           slides: {
               fields: {
                   content: string;
               };
           }[];
       };
   };
   animation: {
       fields: {
           frames: {
               fields: {
                   file: {
                       url: string;
                   };
               };
           }[];
       };
   };
}

Now that we have our data, we can go through and start making our components. We’ll first start with our slider using SwiperJS.

Using Contentful, SwiperJS and GSAP Together: Creating our Slider Component

To get started, head over to your terminal and run the following command to install Swiper:

npm install swiper

In your NextJS app let’s go into our app’s root directory and create a “components” folder. There are two components we will need for our slider, the main Slider component, and a Slide component for each slide in our slider. Let’s start by making our Slide component.

In our components folder, create a new .jsx/.tsx file named “Slide”. In this file we will define a new component called Slide that takes in a single prop. Once again you can use the prop parameter or destructuring, whichever you prefer. This prop variable we want to use will be called “content”. This will be the content we added to our slides in contentful so these will be strings.

Using this component, we want to return an h1 element that displays the content we pass to the component. Our final component should look like this:

export const Slide = ({ content }: SlideProps) => {
   return (
           <h1>{content}</h1>
   );
};

Next up is making our Slider component. Head back to your components folder and make a new .jsx/.tsx file called “Slider”. We will define a new component named Slider and take in a prop we will call “slides”. This will be an array of all our slide objects we get from contentful. Next we want to import the following things:

  • Swiper and SwiperSlide from swiperjs
  • Our Slide component
  • Swiper’s css file

We can do that using the following import statements:

import { Swiper, SwiperSlide } from "swiper/react";
import { Slide } from "./Slide";
import "swiper/css";

In our return statement, we want to return a Swiper component and inside that component, map through all the slides we got passed and render a Slide component inside a SwiperSlide component for each one.

export const Slider = ({ slides }: SliderProps) => {
   return (
       <Swiper spaceBetween={50} slidesPerView={"auto"} centeredSlides={true}>
           {slides.map((slide, index) => (
               <SwiperSlide key={index}>
                   <Slide content={slide.fields.content} />
               </SwiperSlide>
           ))}
       </Swiper>
   );
};

We also have some parameters we passed into the Swiper component. Swiper has many parameters that allow for a lot of customization of their sliders but the three we are using are as follows:

  • spaceBetween – defines how much space should be in between each slide
  • slidesPerView – defines how many slides the user should see at once
  • centeredSlides – makes it so that the active slide is always centered

Now that we have our components, we can add our slider to our Home component in our pages/index file. Our Home component should now look like the following:

const Home = ({ slider, animation }: HomeProps) => {
   return (
       <div>
           <div>
               <Slider slides={slider.fields.slides} />
           </div>
       </div>
   );
};

Next up is making our animation using GSAP.

Using Contentful, SwiperJS and GSAP Together: Making Our Animation With GSAP

Until now we’ve been going pretty easy on the code, but here we step into high gear. Firstly, after our div with our Slider component in it, we want to make another div with the class of “animation” and a canvas element inside of it:

<div className="animation">
   <canvas />
</div>

Next we give this canvas an id of “animation-canvas” and a ref attribute with the value of canvasRef.

<div>
   <canvas ref={canvasRef}  id="animation-canvas"/>
</div>

Now don’t worry, we haven’t created our canvasRef variable just yet but we will right now.

Before our return statement in our component, we want to make a new variable called canvasRef which will use the “useRef” react hook. If you are using typescript, you will want to give this element a type of HTMLCanvasElement or any.

const canvasRef = useRef();

This variable gives us a reference to our canvas element so we can mutate it. Similar to getting a DOM element in vanilla javascript using querySelector.

Our next step is to create a useEffect that will only run in the first render. To do this, import useEffect from react the declare the useEffect function with an empty dependencies array:

import { useEffect } from 'react'

useEffect(() => {
   // function contents
}, [])

Next there are a lot of variables that we need to define so let’s get right into it.

– The frameWidth and frameHeight variables. These will be used to determine how wide and tall the canvas should draw the image frames that we pass it. Basically it’s the size of our animation.

– The canvas and context variables. The canvas variable will be assigned to the current property of our canvasRef the context variable is the return value of the canvas.getContext(‘2d’) function. We also want to define the width and height of our canvas equal to the frameWidth and frameHeight variables we defined earlier, or if you want you can make these different from what we defined for our frames.

const canvas = canvasRef.current
const context = canvas.getContext('2d')
canvas.width = frameWidth;
canvas.height = frameHeight;

– The numFrames variable. This variable is the amount of frames we uploaded which is basically just the length of the frames array we get from Contentful.

   const numFrames = animation.fields.frames.length

– The framesPerSlide variable. This variable is the number of frames that each slide in our slider will hold. We use this because we want our animation to start with the first slide and end perfectly with the last slide.

const framesPerSlide = Math.floor(numFrames / slider.fields.slides.length)

– The transitionSlideIndexes variable. This will be an array that holds the index values that we will use to determine when in the animation we should slide the slider to a new slide. We get these indexes by looping through each frame and checking if that index % numFrames is equal to zero. If it is, that means it’s the start of a new slide.

const transitionSlideIndexes: number[] = [];
for (let i = 0; i < numFrames; i++) {
if (i % framesPerSlide === 0) transitionSlideIndexes.push(i);
}

– The progressPerFrame variable. I will go more into detail about this variable later, but we will use this in our animation to basically tell how far along in the animation it is.

– The frameImages variable. This variable will be an array that holds new Image objects we make for each frame image.

const frameImages: HTMLImageElement[] = [];
for (let i = 0; i < numFrames; i++) {
const image = new Image();
image.src = `https:${animation.fields.frames[i].fields.file.url}`;
frameImages.push(image);
imageProgress.push(progressPerFrame * (i + 1));
}

Note imageProgress is an array defined outside of our useEffect that we will use later, however, you can still add it into this for loop and define the empty array outside of the useEffect function.

Alright! Now that we have all those variables, we can start working on the animation. First up, we have to draw the first frame of the animation to the canvas. To do this we will use the .drawImage function on our context variable.

context.drawImage(frameImages[0], 0, 0, frameWidth, frameHeight);

This function takes in the image we want to draw, its y and x locations on the canvas as well as the width and height we want our image to be.

At this point, we should have the first frame of our animation drawn under our Slider. If it hasn’t appeared right away, restart your development server and the image should be drawn. Next we will get into animating the image as we scroll, however, let’s add some really basic css first.

In our Home component, we want to give our parent <div> a className of “wrapper” and our slider <div> will get the className of “slider”. Next, in our root directory, we should have a folder generated by NextJS called styles. In that folder we want to open up the globals.css file. We can get rid of whatever is in it and add the following css styles.

/ This is a simple reset
*{
 margin: 0;
 padding: 0;
 box-sizing: border-box;
}
/*
   gives our wrapper a display of flex so
   the slider and animation appear next
   to each other. We also define a max
   width and margin with auto on the x-axis
   so the entire wrapper is centered on the
   page.

   We defined a height of 300vh just so we can
   scroll to preview the animation without
   having to add other content to increase
   the page height.
*/
.wrapper{
 display: flex;
 justify-content: space-between;
 height: 300vh;
 max-width: 1200px;
 margin: 0 auto;
}
// gives our slider a max width of 500px
.slider {
 max-width: 500px;
}

Now that we added these styles, we should have our slider and animation displayed next to each other. With this done, we can get into adding GSAP to make the animation play as we scroll.

Adding GSAP

Our first order of business is to install gsap. You can do this by running the following command in your terminal:

npm install gsap

Once we have gsap installed, we can go into our page/index file and import it.

import gsap from "gsap"

Along with gsap, we want to import the ScrollTrigger plugin:

import ScrollTrigger from "gsap/dist/ScrollTrigger"

And we will want to register this plugin with gsap to let it know we will be using it. We can do this by using the registerPlugin function on our gsap object we imported.

gsap.registerPlugin(ScrollTrigger)

Making our first Tween

A tween in gsap is what does all the animation work we define in it. Using the properties that we set, the tween figures out what each property should be at each point and applies it. GSAP has 3 common methods for creating a tween

  • gsap.to()
  • gsap.from()
  • gsap.fromTo()

If you’d like to dive deeper into learning about tweens you can check out GSAP’s documentation page on it here.

In our case, we will be using the gsap.to() method of creating a tween. We will create our tween and the first property we will assign it is a reference to the element we want it to animate. This can be a reference using the class or id of the element we want. Our first Tween will be targeting the canvas that we use for displaying each animation frame so we will pass in the id of that canvas which should be “animation-canvas”.

gsap.to("#animation-canvas")

Next parameter we will pass is an object that will hold all the properties we want to change, however, for this canvas, we aren’t necessarily changing properties, but we want to update the image that is being shown. Sadly we can’t just target a src property, however, a way to update the image is to completely erase the canvas and draw whichever image we want next. Luckily, we can do this and keep it in sync with the users scrolling by using the ScrollTrigger plugin.

So, inside the object we are passing as a parameter, we want to make a property called “scrollTrigger”. This property will have an object as a value that has the following properties:

  • “trigger” – this is the element that is used to determine when the animation starts
  • “start” – this determines the starting position of the scrollTrigger. This takes in a string with two values in it that can be either “top”, “center” or “end”. This string describes where on the trigger and on the scroller must meet in order to start the ScrollTrigger. So for example, “top center” means when the top of the trigger meets the center of the scroller
  • “endTrigger” – this is the element that determines when the animation should end

In our case, we want these three properties to have the following values:

gsap.to("#animation-canvas", {
   scrollTrigger: {
       trigger: "#animation-canvas",
       start: "top top",
       endTrigger: ".slider"
   }
})

Our final property we want to add is an onUpdate property which will have a value of an async function. This function will run on every update, meaning every scroll which is how we can continuously check for when we need to update the animation frame as the user scrolls.

gsap.to("#animation-canvas", {
   scrollTrigger: {
       trigger: "#animation-canvas",
       start: "top top",
       endTrigger: ".slider",
       onUpdate: async (self) => {}
   }
})

Our first objective in this function is to determine what frame we should currently be on in the animation. We will do this by creating a function called getFrameIndex and we will pass in two properties. The first is the current progress of the animation which we get from the “self” prop we passed into the onUpdate function and the second is our starting index which is 0 since we are always starting on the first frame.

Using these two properties, we will make a recursive function that continuously checks the progress with our record of which frame should show at which progress point. We made this record earlier in the project and it was our imageProgress array. If you didn’t do it earlier I’ll quickly explain how to get this array and populate it.

Firstly, outside of our useEffect, we want to create an array called imageProgress and assign it to an empty array. Next, inside our useEffect, we have a for loop that we used to create new Image objects and populate an array with all our frame images. Well, inside that same for loop, we want to push the product of our progressPerFrame variable and the current loop index + 1. What we are doing is taking how much progress defines the distance between any 2 frames and multiplying it by the current frame to figure out the exact progress point at which that specific frame starts, and, since we set these frames in order in Contentful, we can use their indexes in our imageProgress array as their order without having to worry about referencing the wrong frame.

Alright, now that we have that array, let’s get into making this getFrameIndex function. Firstly, we want to check if the index we were given is greater than the final index in our array of frames. If this is the case, then that means we’re at the last frame so we should be at the end of the animation, so we just return the index we got passed and that’s the frame we show.

const getFrameIndex = (progress, index) => {
   if (index >= animation.fields.frames.length) return index;
}

If we aren’t at the last frame, then we must still be in the animation, so, we want to check to see if our current progress that we passed is greater than the progress point of the frame whose index we passed. If it is, that means we are behind on the animation, so we return the value of another getFrameIndex function call, except this time we pass the index we currently have but plus 1. We pass the same progress. This is why we needed a recursive function so the function can check as many times as it needs without us having to define how many times to check manually.

const getFrameIndex = (progress, index) => {
   if (index >= animation.fields.frames.length) return index
   if (progress > imageProgress[index]) {
       return getFrameIndex(progress, index + 1);
   }
}

Next, is a check to see if progress is less than the progress point we defined for the frame index we got passed. If it is, that means we’ve found the next frame we should go to so we can return our index.

const getFrameIndex = (progress, index) => {
   if (index >= animation.fields.frames.length) return index;
   if (progress > imageProgress[index]) {
       return getFrameIndex(progress, index + 1)
   }else if (progress < imageProgress[index]) {
       return index;
   }
}

Lastly, as a default in case somehow none of these conditionals are met, we can simply return the index for the last frame so our project doesn’t crash.

const getFrameIndex = (progress: number, index: number): number => {
   if (index >= animation.fields.frames.length) return index;
   if (progress > imageProgress[index]) {
       return getFrameIndex(progress, index + 1);
   } else if (progress < imageProgress[index]) {
       return index;
   }

   return animation.fields.frames.length - 1;
};

Now that we have our function to get the frame we are on, we can assign our frame variable equal to whatever we get back from our function inside our scrollTriggers onUpdate function.

const frame = await getFrameIndex(self.progress, 0)

Next up is updating the canvas to show the correct frame. We can do this in two very easy steps now that we know what frame we should be on. Our first step is completely clearing the canvas. We will erase everything and anything that is currently drawn on it. To do this we can use the context.clearRect function. This function takes in starting points for where we want to start the clear and distances from the starting point in each direction to determine how far and wide we want to clear from the starting point. Since we are clearing everything, our starting points can just be 0,0 and the distances can be the width and height of the canvas.

context.clearRect(0, 0, canvas.width, canvas.height)

Our next step is to just draw our new frame image. We do this the same way we drew our first frame earlier in our project, using the context.drawImage function.

context.drawImage(frameImages[frame], 0, 0, frameWidth, frameHeight)

Before our animation is done, we need to add two more tweens.

These tweens will target the .slider and .animation elements and give them each a scrollTrigger. We want to add a scrollTrigger to this element because we want it to pin to the screen as the user scrolls and the animation plays.

gsap.to(".slider", {
   scrollTrigger: {
       trigger: ".slider",
       start: "top top",
       end: "bottom top",
       markers: true,
       pin: true,
   }
})

gsap.to(".animation", {
   scrollTrigger: {
       trigger: ".animation",
       start: "top top",
       endTrigger: ".slider",
       markers: true,
       pin: true,
   }
})

A new property we are using, besides the pin, is the markers property. This is something gsap gives us that helps us visualize the start and end points for our animation. This should only be used in the development of the animation.

With these final 2 tweens added, you should now be able to view your animation as you scroll!

Congrats you’ve officially made an animation with GSAP! If you also notice, the functionality we used allows the animation to play in reverse as we scroll back up as well.

Now that we have our animation, we’re almost done with the project. We just need to somehow tell our slider when to update and switch to a new slide. The way we will do this is by figuring out which direction the user is scrolling, then checking if we even need to update the slide we are on. If we do we’ll dispatch an event that should update the slide and if we don’t then we can just leave everything as is. Sounds simple enough right!

Well, don’t worry, we’ll go through each step to really see how everything is working.

Which direction are we scrolling

Luckily this step is a lot simpler than it would seem, thanks to scrollTrigger! The self property we get in our onUpdate function has another property called direction. The value of this property lets us know if we are scrolling forwards with a 1 and if we are scrolling backwards with a -1. With this, we can just use a conditional to tell the function what to do based on the direction.

if (self.direction > 0){
   // Update to the next slide functionality
} else {
   // Update to the previous slide functionality
}

Checking if we need to update

It would be quicker to just update to the next slide, however, we want each slide to show relative to where the animation is in its progress. Meaning the middle slide should be showing in the middle of the animation, the first slide at the start and the last slide at the end. To do this, we will create a function called checkForCardUpdate. This function will take in the current frame, the nextIndex which is a variable we want to define prior to our tween and give it an initial value of 1, the transitionSlideIndexes array, which is an array that contains the indexes that represent a starting point for a slide, and the direction we are scrolling.

This function will check our direction, and depending on the direction, it will check if the frame has passed or is equal to the frame of the next transitionSlideIndex at index of nextIndex. If it has, we will return true letting us know we need to update the slides, otherwise it will return false.

const checkForCardUpdate = () => {
   if (direction > 0){
       if (frame >= transitionSlideIndexes[nextIndex]) {
           return true
       } else {
           return false
       }
   } else {
       if (frame <= transitionSlideIndexes[nextIndex]) {
           return true
       } else {
           return false
       }
   }
}

We will assign this return value to a variable called updateCard and if it is true, we will create and dispatch a custom event. If we are scrolling forwards we will call this event “slideNext” and if we are traveling backwards we will call it “slidePrev”. Before dispatching the events, we want to add the nextIndex variable we are using as a property on the event that we will use later.

Lastly, after dispatching the event, we want a conditional to check if we should update the nextIndex variable we are using. If we are scrolling forwards, we want to check if nextIndex is less than the final index of our transitionSlideIndexes function. If we are scrolling backwards, we want to check if it is greater than 0. If any of these conditionals are met, we will increase or decrease the nextIndex variable respectively.

if (self.direciton > 0){
   const updateCard = checkForCardUpdate(frame, nextIndex, transitionSlideIndexes, self.direction)

   if (updateCard) {
       const slideNextEvent = new Event("slideNext");
       slideNextEvent["nextIndex"] = nextIndex;
       document.dispatchEvent(slideNextEvent)

       if (nextIndex < transitionSlideIndexes.length - 1) nextIndex++
   }
} else {
   const updateCard = checkForCardUpdate(frame, nextIndex, transitionSlideIndexes, self.direction)

   if (updateCard) {
       const slidePrevEvent = new Event("slidePrev");
       slidePrevEvent["nextIndex"] = nextIndex;
       document.dispatchEvent(slidePrevEvent)

       if (nextIndex > 0) nextIndex--
   }
}

Listening for the dispatched events

Our final mission is to listen for these events and trigger a slide to the index we were passed. To do this we will make a new component called AnimationEventListener and put this component inside our Swiper component in our Slider file. This component won’t return any jsx so nothing will visually be rendered to the user, however, this component will have a useEffect that adds eventListeners for our custom events. We will also want to import a hook given to us by swiper called useSwiper. This hook lets us programatically update our Swiper Slider which is how we will update the slides based on the events. We will create a swiper variable and assign it to this useSwiper hook.

To handle adding and removing the event listeners, we will make two functions. One called addListeners and another called removeListeners. Inside our addListeners function we will addEvent listeners for our events, however, we will also define a function called slide where we will take the nextIndex property we passed with the event and use that to update the slide. To update the slide we will use the slideTo function we get from useSwiper. Before just trying to slide to the next slide, we want to write a conditional to check that our swiper.params are not undefined. This lets us know that we are actually getting the data about our Slider and our useSwiper hook isn’t giving us bad information.

Lastly, in our useEffect, we first want to add our swiper variable as a dependency so the useEffect will run every time the swiper object is updated. We want to do this because our useSwiper hook doesn’t give us the information on our Slider on the first render. Initially it’s empty except for a destroyed property, however, on the next render it is updated with our Slider.

Note the useSwiper hook must be used inside a Swiper component so it knows which Swiper component to get its data from.

Once we add swiper as a dependency, we want to write a conditional to check that swiper.destroyed is not equal to true. If it is, it means we don’t have the correct data about our Swiper component, so we shouldn’t add the event listeners. However, if it is false that means we have the information we need to listen for events and update the swiper slides when we need to. Lastly we return a cleanup function in our useEffect that just calls the removeListeners function we created.

import { useEffect } from "react"
import { useSwiper } from "swiper/react"

export default function AnimationEventListener() {
   const swiper = useSwiper();

   const addListeners = () => {
       const slide = async (e: any) => {
           const nextIndex = e["nextIndex"]
           if (swiper.params !== undefined) {
               swiper.slideTo(nextIndex)
           }
       }

       document.addEventListener("slideNext", (e) => slide(e))
       document.addEventListener("slidePrev", (e) => slide(e))
   }

   const removeListeners = () => {
       document.removeEventListeners("slideNext", () => {})
       document.removeEventListeners("slidePrev", () => {})
   }

   useEffect(() => {
       if (!swiper.destroyed) {
           addListeners();
       }

       return () => {
           removeListeners();
       }
   }, [swiper])

   return <></>
}

With that, we can just add this component inside our Swiper component in our Slider file and we’re done!

Using Contentful, SwiperJS and GSAP Together: Conclusion

Making different technologies work together can seem very scary at first, and even impossible but eventually it’s very possible to find a way, no matter how complicated that may be at first. I hope this project was able to help some of you! Reach out if you have any questions!