Cube Slider Animation with GSAP and JavaScript

in tutorials by Mirza Hodzic12 min read
Cube Slider Animation with GSAP and JavaScript
DEMOGET CODE

Recently, I was surfing the internet and looking at what interesting things other developers are making and sharing as open-source, hoping to find inspiration for my next tutorial. And I found many good examples, but one particularly impressed me. It is an animation, which I found on Codepen, called Hello From Dublin!, and created by the ingenious Pete Barr (you can find the link to the animation and the original design in the Credits section). Since I generally love sliders of all shapes and kinds, I thought... this could be made into a cool slider, and here we are...

As I already mentioned in the paragraph above, this time we will try to create a slider, which I have named Cube Slider because of the appearance of the section where the texts we will animate are located.

As usual, I first decided to quickly create a sketch that will give us an approximate idea of how it will all look.

Cube Slider Animation with GSAP and JavaScript - Sketch

In the sketch, you can see that the slider will consist of several parts, including the main central part where we will place, design, and animate our main "Hello from..." texts. Then there will be an additional smaller part that we will place to the left and right of the central div element, which we will also animate separately. The last but not insignificant part, which we will deal with later, and which you can also see in the sketch above, is the navigation that we will use to navigate through our slider.

So many words spent at the very beginning, and I haven't said anything useful yet. 🫣 Let's get straight to the point.

HTML Part

The HTML structure is designed in such a way that the central part of our tutorial is placed in one parent element, and the navigation is separated and placed in another parent element. Let's first look at the structure, and then below the following code, we will explain the entire structure in more detail.

Central section

1<div class="container">
2  <div class="hi" data-lat="51.5098° N" data-lang="-0.1180° E">
3    <div class="hi__cuboid">
4      <div class="face face--front">
5        <p class="hi__word">Hello</p>
6      </div>
7      <div class="face face--back">
8        <p class="hi__word">Hello</p>
9      </div>
10      <div class="face face--top">
11        <p class="hi__word">Hello</p>
12      </div>
13      <div class="face face--bottom">
14        <p class="hi__word">Hello</p>
15      </div>
16    </div>
17    <div class="hi__cuboid">
18      <div class="face face--front">
19        <p class="hi__word">From</p>
20      </div>
21      <div class="face face--back">
22        <p class="hi__word">From</p>
23      </div>
24      <div class="face face--top">
25        <p class="hi__word">From</p>
26      </div>
27      <div class="face face--bottom">
28        <p class="hi__word">From</p>
29      </div>
30    </div>
31    <div class="hi__cuboid">
32      <div class="face face--front">
33        <p class="hi__word">London</p>
34      </div>
35      <div class="face face--back">
36        <p class="hi__word">London</p>
37      </div>
38      <div class="face face--top">
39        <p class="hi__word">London</p>
40      </div>
41      <div class="face face--bottom">
42        <p class="hi__word">London</p>
43      </div>
44    </div>
45  </div>
46  <!-- ... other slides ... -->
47
48  <div class="hi__base">
49    <div class="hi__base-plate"></div>
50    <p class="hi__location hi__location--lat">53.3454° N</p>
51    <p class="hi__location hi__location--long">-6.3070° E</p>
52  </div>
53</div>

Our parent element with the class .container will actually be the holder of our slider (slideshow) and will consist of two parts.

We will place our slides, which we will animate with a "cube" animation on click of the PREV or NEXT button, in the first part, a div element with the class .hi. Each of these div elements will contain its main child element, specifically a child element with the class .hi__cuboid, through which we will create our cube element.

On this .hi element, we will place information about latitude (LAT) and longitude (LANG) that we will update each time the active slide changes. The information itself will be stored in data attributes data-lat and data-long.

Each cube element will consist of four child elements, each representing one side of our cube, designated in our code by the class .face. However, each of these face elements will additionally have its unique class, which I want to list here in order: .face--front, .face--back, .face--top, and .face--bottom. We will design these so that together they form a cube in reality.

It's also important to note here that for each slide, we will have three words that we will animate simultaneously, each made in the way I explained a few seconds ago.

The second part located in this section is the element with the class .hi__base. In this part, we will place additional text, specifically latitude (LAT) and longitude (LONG) information about the city whose slide is currently active. This part will not have a standard rotation animation; instead, we will create an additional character shuffle animation specifically for these two texts.

Navigation

1<div class="slider-controls">
2  <button class="slider-prev">Prev</button>
3  <button class="slider-next">Next</button>
4</div>

A very simple part and I think there's not much to explain here. In this element with the class .slider-controls, we have two buttons that will navigate us through our slider. This part will get a bit more complicated in the JavaScript section, but don't worry... we've left all the challenging parts for the end. 😃

Styling Elements

As it usually happens, designing the application, website, and other elements tends to be almost the longest part of the project. However, since important things also happen during the design phase, I will try to break down this part into smaller sections and explain each of them in a few sentences. Nothing complicated happens in this phase, and anyone with basic CSS knowledge will be able to understand what is being discussed.

Custom Font Definition

1@font-face {
2  font-family: 'Inter Variable';
3  src: url(https://cdn.jsdelivr.net/fontsource/fonts/inter:vf@latest/latin-wght-normal.woff2) format('woff2-variations');
4  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
5  font-stretch: 100% 900%;
6  font-weight: 100 900;
7  font-display: swap;
8}

In this part, we define a custom font using the @font-face rule. We specify the font family name, the source URL for the font file, and the character range that the font supports. Additionally, we set properties for the font's stretch, weight, and display. The inclusion of font-display: swap secures that text remains visible during the font loading process.

Root Variables

1:root {
2  --grey: #5e5d5e;
3  --mid-grey: #3f3f3f;
4}

The :root selector is where we define custom properties (variables) for colors that are applied across the entire CSS stylesheet. These variables are instrumental in maintaining and updating color values consistently throughout the design.

Box Sizing and Global Styles

1html {
2  box-sizing: border-box;
3}
4
5*, *:before, *:after {
6  box-sizing: inherit;
7}
8
9html, body {
10  width: 100%;
11  height: 100%;
12  margin: 0;
13  padding: 0;
14}

In our styling approach, we set box-sizing: border-box universally across all elements, including *:before and *:after. This choice secures that padding and borders are included within each element's specified width and height, rather than adding to them. This method secures predictability in layout design, making it easier to manage spacing and alignment consistently throughout the application.

Additionally, we define the html and body elements to occupy the entire viewport without any default margin or padding.

Body Styles

1body {
2  display: flex;
3  align-items: center;
4  justify-content: center;
5  background-color: black;
6  font-family: 'Inter Variable';
7  color: white;
8  -webkit-font-smoothing: antialiased;
9  -moz-osx-font-smoothing: grayscale;
10  overflow: hidden;
11  background-size: 50px 50px;
12  background-position: center;
13  background-image: linear-gradient(to right, var(--mid-grey) 1px, transparent 1px), linear-gradient(to bottom, var(--mid-grey) 1px, transparent 1px);
14}

We configure the body element to use our custom font and center its content using Flexbox. The background is set to black with a grid pattern for visual appeal. Font smoothing is applied to secure text clarity. Additionally, we use overflow: hidden to prevent scrollbars from appearing, securing a clean and uninterrupted user interface.

Container Styles

1.container {
2  position: relative;
3  width: 100%;
4  height: 100%;
5  display: flex;
6  align-items: center;
7  justify-content: center;
8  perspective: 900px;
9  visibility: hidden;
10}

We use the .container class to create a flex container that centers its child elements and applies a 3D perspective effect. The visibility: hidden property is typically used to hide elements initially, revealing them later through animations or interactions.

Hi Class and Cuboid Styles

1.hi {
2  position: relative;
3  z-index: 1;
4  font-size: 100px;
5  font-stretch: 400%;
6  font-weight: 100;
7  line-height: 1;
8  text-transform: uppercase;
9  text-align: center;
10  transform-style: preserve-3d;
11}
12
13.hi__cuboid {
14  position: relative;
15  width: 500px;
16  height: 70px;
17  transform-style: preserve-3d;
18  margin: 30px 0;
19  display: none;
20  opacity: 0;
21}

We style the .hi class to handle the main greeting text. It positions the text relatively, securing a high z-index for layering, and applies large font properties for visibility. The .hi__cuboid class is responsible for styling cuboid elements that contain text faces. It arranges these faces in 3D space using transform-style: preserve-3d, maintaining their 3D positioning for visual effects.

Cuboid Faces

1.hi__cuboid .face {
2  position: absolute;
3  left: 0;
4  top: 0;
5  background-color: black;
6}
7
8.hi__cuboid .face--front,
9.hi__cuboid .face--back,
10.hi__cuboid .face--top,
11.hi__cuboid .face--bottom {
12  width: 500px;
13  height: 70px;
14}
15
16.hi__cuboid .face--front {
17  transform: translateZ(calc(70px / 2));
18}
19
20.hi__cuboid .face--back {
21  transform: translateZ(calc(70px / 2 * -1)) rotateY(180deg) rotate(180deg);
22}
23
24.hi__cuboid .face--left {
25  width: 70px;
26  height: 70px;
27  transform: translateX(calc(70px / 2 * -1)) rotateY(-90deg);
28}
29
30.hi__cuboid .face--right {
31  width: 70px;
32  height: 70px;
33  transform: translateX(calc(500px - 70px / 2)) rotateY(90deg);
34}
35
36.hi__cuboid .face--top {
37  transform: translateY(calc(70px / 2 * -1)) rotateX(90deg);
38}
39
40.hi__cuboid .face--bottom {
41  transform: translateY(calc(70px - 70px / 2)) rotateX(-90deg);
42}
43
44.hi__cuboid.active {
45  display: block;
46  opacity: 1;
47}

In this section, we define the size and position of each side of the cuboid in 3D space. Each side (front, back, top, bottom) is precisely positioned and transformed using CSS. This secures that the cuboid elements are arranged correctly and interact effectively within the 3D space defined by the CSS transformations.

Face Content Styles

1.face {
2  display: flex;
3  align-items: center;
4  justify-content: center;
5  overflow: hidden;
6}
7
8.face.face--top,
9.face.face--bottom {
10  background: white;
11  color: black;
12}

In this section, the .face class secures that its content is centered using Flexbox. Additional styles are applied to specific faces like the top and bottom faces to set their background and text colors.

Word and Base Styles

1.hi__word {
2  margin: 0;
3  transform: translateY(0);
4}
5
6.hi__base {
7  position: absolute;
8  z-index: 0;
9  top: 50%;
10  left: 50%;
11  transform: translate(-50%, -50%);
12  width: calc(100% - 3rem);
13  max-width: 752px;
14  height: 250px;
15}
16
17.hi__base-plate {
18  width: 100%;
19  height: 100%;
20  background: black;
21  border: 1px solid var(--grey);
22}
23
24.hi__location {
25  position: absolute;
26  margin: 0;
27  font-size: 20px;
28  font-stretch: 400%;
29  font-weight: 400;
30  min-width: 126px;
31  text-align: center;
32}
33
34.hi__location--lat {
35  top: 50%;
36  left: 0vw;
37  transform: rotate(-90deg) translateX(10px);
38}
39
40.hi__location--long {
41  top: 50%;
42  right: 0vw;
43  transform: rotate(90deg) translateX(-10px);
44}

We manage the appearance of text inside the cuboids with the .hi__word class, securing it fits and looks appropriate within the 3D elements. The .hi__base class is responsible for laying out and styling the base plate and displaying location data effectively.

Home Logo and Collection Styles

1.home-logo {
2  width: 20vw;
3  max-width: 150px;
4  position: fixed;
5  bottom: 15px;
6  right: 15px;
7  color: white;
8  text-align: right;
9}
10
11.collection {
12  position: fixed;
13  z-index: 1000;
14  top: 24px;
15  right: 24px;
16  display: flex;
17  flex-direction: column;
18}
19
20.collection.collection__bottom {
21  top: inherit;
22  bottom: 24px;
23}
24
25.collection__link {
26  position: relative;
27  margin-bottom: 16px;
28  color: white;
29  text-decoration: none;
30  font-size: 16px;
31}
32
33.collection__link span {
34  display: block;
35  position: absolute;
36  bottom: -3px;
37  left: 0;
38  height: 1px;
39  width: 10%;
40  background-color: white;
41  content: "";
42  transition: width 0.3s;
43}
44
45.collection__link:hover {
46  text-decoration: none;
47}
48
49.collection__link:hover span {
50  width: 100%;
51}

We style a fixed-position logo at the bottom right with the .home-logo class. Additionally, the .collection class positions a fixed element for navigation links, styles the links, and adds hover effects. This part is not important at all in our tutorial, but it's here, so as not to confuse you.

Slider Controls Styling

1.slider-controls {
2  position: fixed;
3  bottom: 50%;
4  transform: translateY(50%);
5  display: flex;
6  justify-content: space-between;
7  width: 100%;
8  padding: 0 20px;
9}

We use the .slider-controls class to style fixed-position navigation buttons located at the bottom of the viewport. This class utilizes flexbox (display: flex;) to evenly distribute the buttons (justify-content: space-between;). Adding padding: 0 20px; creates space around the buttons for better spacing and alignment.

Button Styling

1.slider-controls button {
2  background: none;
3  border: 1px solid white;
4  color: white;
5  padding: 10px 20px;
6  cursor: pointer;
7}

We style the navigation buttons using the .slider-controls button selector. These buttons are designed with a transparent background, a white border, white text for visibility, padding to provide spacing around the text, and a pointer cursor to indicate interactivity.

Media Query for Responsive Design

1@media screen and (max-width: 960px) {
2  .container {
3    flex-direction: column;
4  }
5
6  .hi {
7    font-size: 60px;
8  }
9
10  .hi__cuboid {
11    width: 300px;
12    height: 50px;
13    margin: 75px 0;
14  }
15
16  .hi__base {
17    height: 520px;
18  }
19
20  .slider-controls {
21    width: auto;
22    bottom: 75px;
23    transform: translateY(0);
24  }
25
26  .slider-controls button {
27    margin: 0 0.325rem;
28  }
29}

At @media screen and (max-width: 960px), we adjust our layout for smaller screens. We make .container switch to a column layout, reduce the font size of .hi, resize .hi__cuboid dimensions, adjust the height of .hi__base, and reposition .slider-controls to fit within the viewport.

JavaScript Code Breakdown

In this part, we will add all functionalities and animations to our static, nicely designed slider (slideshow). For animations, we will once again use the excellent GreenSock (GSAP) library. The JavaScript part will be divided into several functions, and we will explain each of them in detail in the following section.

If you are already familiar with GSAP, you will have no trouble following this part of the tutorial. However, if you are new to programming and don't know what GreenSock (GSAP) actually is, I recommend scrolling to the end of the page and finding the link in the Credits that leads to their website. Take a look at their Documents page.

Variable Initialization

Here we will initialize the application by setting up various variables and selecting necessary DOM elements using helper functions (select and selectAll). These functions streamline the process of accessing elements from the HTML document, making our code more concise and readable.

Next, we will initialize the application state and dimensions. This includes setting initial values for variables such as container, hiElements, baseLat, baseLang, prevButton, nextButton, winW, winH, pointer, currentIndex, isAnimating, and rotationTimeline. These variables are crucial for managing the application's state, tracking dimensions, and handling user interaction.

1console.clear();
2
3const select = e => document.querySelector(e);
4const selectAll = e => document.querySelectorAll(e);
5
6const container = select('.container');
7const hiElements = selectAll('.hi');
8const hiWords = selectAll('.hi__word');
9const baseLat = select('.hi__location--lat');
10const baseLang = select('.hi__location--long');
11const prevButton = select('.slider-prev');
12const nextButton = select('.slider-next');
13let winW = 0;
14let winH = 0;
15let pointer = {
16  x: window.innerWidth / 2,
17  y: window.innerHeight / 2
18};
19let currentIndex = 0;
20let isAnimating = false;
21
22let rotationTimeline;

Then, we will define the init() function. This function prepares the application for display and animation.

Initialization Functions

1function init() {
2  setWinDimensions();
3  gsap.set(container, {
4    autoAlpha: 1
5  });
6
7  const initialHi = hiElements[currentIndex];
8  const initialCuboids = initialHi.querySelectorAll('.hi__cuboid');
9
10  gsap.set(initialCuboids, {
11    display: 'block',
12    autoAlpha: 0
13  });
14
15  gsap.timeline({
16      delay: 0.5
17    })
18    .from('.hi__location--lat', {
19      x: 100,
20      autoAlpha: 0,
21      ease: 'power4',
22      duration: 1
23    })
24    .from('.hi__location--long', {
25      x: -100,
26      autoAlpha: 0,
27      ease: 'power4',
28      duration: 1
29    }, 0)
30    .from(initialCuboids, {
31      y: winH,
32      duration: 3,
33      stagger: 0.14,
34      ease: 'elastic(0.4,0.3)'
35    }, 0)
36    .to(initialCuboids, {
37      autoAlpha: 1,
38      duration: 0,
39      stagger: 0
40    }, 0);
41
42  startRotation();
43}

In this function, we initialize the application by first setting the initial window dimensions using setWinDimensions(). This secures that the application is responsive and adapts correctly to the user's screen size. Next, we fade in the main container (container), progressively making it visible to the user.

Following that, we animate key elements such as hi__location--lat, hi__location--long, and hi__cuboid using GSAP timelines. These animations are carefully sequenced with a slight delay (delay: 0.5) and employ easing functions (ease: 'power4') to achieve smooth and attractive transitions.

Lastly, init() initiates the rotation animation (startRotation()), which continuously rotates elements within the application. This animated rotation adds dynamism and secures the interactive nature of the application.

1function startRotation() {
2  if (rotationTimeline) {
3    rotationTimeline.kill(); // Kill existing timeline if it exists
4  }
5
6  rotationTimeline = gsap.timeline({
7    repeat: -1
8  });
9
10  rotationTimeline.to('.hi__cuboid', {
11    rotateX: '+=360',
12    duration: 8,
13    ease: 'none',
14    yoyo: true,
15    repeat: -1,
16  });
17
18  rotationTimeline.fromTo('.hi__cuboid', {
19    rotateY: 8,
20    rotate: -10
21  }, {
22    rotateY: -8,
23    rotate: 10,
24    duration: 2.2,
25    yoyo: true,
26    repeat: -1,
27    ease: 'sine.inOut'
28  }, 0);
29}

In this part, we set up a GSAP timeline (rotationTimeline) dedicated to animating continuous rotations of .hi__cuboid elements within the application. This timeline is important for keeping an exciting look that makes users more interested and involved.

The timeline defines rotations along both the X and Y axes, securing that the elements rotate smoothly and continuously. This motion is configured with specific durations and easing functions tailored to achieve the desired visual appeal and smoothness. By using GSAP's features, we secure that the rotation animation is smooth and adds an exciting element to how users experience everything.

1function setWinDimensions() {
2  winW = window.innerWidth;
3  winH = window.innerHeight;
4}

In this part, we maintain the accuracy and responsiveness of animations and calculations that rely on window dimensions (winW and winH). The function updates winW and winH whenever the window is resized, ensuring that these variables accurately reflect the current dimensions of the browser window.

By dynamically adjusting winW and winH based on window resizing events, the application can adapt smoothly to changes in screen size. This responsiveness secures that animations and layout calculations remain precise and looks good on different devices and screen sizes, making the user experience better with smooth and consistent interactions.

Calculation and Animation Functions

1function calcOffset(xPos, yPos) {
2  let dX = 2 * (xPos - winW / 2) / winW;
3  let dY = -2 * (yPos - winH / 2) / winH;
4  return [dX, dY];
5}

We compute the offset of the cursor (pX, pY) relative to the center of the screen (winW and winH). This involves calculating dX and dY values derived from the cursor's position, which indicate how far the cursor is from the center along the horizontal and vertical axes.

1function followPointer(pX, pY) {
2  let nPos = calcOffset(pX, pY); // get cursor position from center
3	let nY = nPos[1];
4	let positiveY = Math.sqrt(nY*nY);
5  let deltaW = 600*positiveY;
6
7	gsap.to(hiWords, {
8    fontWeight: 900-deltaW,
9		duration: 0.75
10	});
11}

We adjust the CSS property (fontWeight) of hiWords based on the cursor position (pX, pY). Calculating deltaW involves assessing the vertical cursor position relative to the screen center (positiveY).

1function shuffleText(element, newText) {
2  const originalText = element.textContent;
3  const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
4  let shuffledText = '';
5  let iterations = 0;
6  const shuffleInterval = setInterval(() => {
7    // Generate shuffled text with original characters in random order
8    shuffledText = originalText.split('').sort(() => Math.random() - 0.5).join('');
9    element.textContent = shuffledText;
10    if (iterations > originalText.length * 2) {
11      clearInterval(shuffleInterval);
12      element.textContent = newText;
13    }
14    iterations++;
15  }, 25);
16}

We animate the shuffling effect of text for specified elements (element). This function generates a randomized version of the original text (originalText) by sorting characters randomly and smoothly transitions it to display newText.

Slide Navigation and Event Handling

1function changeSlide(next = true) {
2  if (isAnimating) return;
3  isAnimating = true;
4
5  const oldIndex = currentIndex;
6  currentIndex = next ? (currentIndex + 1) % hiElements.length : (currentIndex - 1 + hiElements.length) % hiElements.length;
7
8  const oldHi = hiElements[oldIndex];
9  const newHi = hiElements[currentIndex];
10
11  const oldCuboids = oldHi.querySelectorAll('.hi__cuboid');
12  const newCuboids = newHi.querySelectorAll('.hi__cuboid');
13
14  const newLat = newHi.getAttribute('data-lat');
15  const newLang = newHi.getAttribute('data-lang');
16
17  // Kill previous rotation animation to avoid resetting the state
18  rotationTimeline.kill();
19
20  gsap.set(oldCuboids, {
21    display: 'none',
22    autoAlpha: 0
23  });
24
25  gsap.set(newCuboids, {
26    display: 'block',
27    autoAlpha: 0
28  });
29
30  gsap.timeline().to(newCuboids, {
31    autoAlpha: 1,
32    duration: 0,
33    stagger: 0,
34  });
35
36  gsap.timeline().to(newCuboids, {
37    rotateX: '+=360 * 4',
38    rotateY: 8,
39    rotate: -10,
40    duration: 1.75,
41    ease: 'elastic(0.4,0.3)',
42    stagger: 0,
43    onComplete: () => {
44      shuffleText(baseLat, newLat);
45      shuffleText(baseLang, newLang);
46      isAnimating = false;
47      // Restart the rotation animation after slide change
48      startRotation();
49    }
50  });
51}

We will handle navigation between slides (hiElements) based on the next parameter. This function manages the animation state (isAnimating), updates the current slide index (currentIndex), and orchestrates transitions between the old and new slides using GSAP animations. This ensures smooth visual changes by fading out the previous slide (oldCuboids) and fading in the new slide (newCuboids).

Additionally, we will rotate the new slide (newCuboids) along multiple axes (rotateX, rotateY, rotate) for a duration of 1.75 seconds with an elastic easing effect (elastic(0.4,0.3)). After completing the slide transition, we will animate and update textual content (baseLat and baseLang) using shuffleText.

Once all animations are finished, we will reset the isAnimating flag and restart the rotation animation (startRotation()), ensuring a seamless user experience throughout slide navigation.

Event Listeners and Initialization

We will add event listeners (mousemove, touchmove, touchstart, click) to track user interactions such as cursor movements, touch events, and button clicks.

1window.addEventListener("mousemove", function(event) {
2  pointer.x = event.clientX;
3  pointer.y = event.clientY;
4  followPointer(pointer.x, pointer.y);
5});
6
7window.addEventListener('touchmove', function(event) {
8  pointer.x = event.touches[0].clientX;
9  pointer.y = event.touches[0].clientY;
10  followPointer(pointer.x, pointer.y);
11});
12
13window.addEventListener('touchstart', function(event) {
14  pointer.x = event.touches[0].clientX;
15  pointer.y = event.touches[0].clientY;
16  followPointer(pointer.x, pointer.y);
17});
18
19prevButton.addEventListener('click', () => changeSlide(false));
20nextButton.addEventListener('click', () => changeSlide(true));
21
22window.onload = () => {
23  init();
24};

These listeners will trigger corresponding functions (followPointer, changeSlide) to handle user input and update the application's state accordingly. Additionally, the window.onload event will guarantee that the init() function executes once all resources are loaded, initializing the demo and start animations.

And that's it! I hope you've enjoyed it and that this will serve as a foundation for your next project or inspire you for an even better one. Enjoy, share the article with friends, and visit us again.

Credits

  • The inspiration for effects and Layout comes from Pete Barr
  • GSAP by Greensock
  • Inter by Google Fonts

More like this

Ready for more? Here are some related posts to explore