Build an asymmetric JavaScript slideshow with CSS Grid & GSAP

Without further ado, here’s what we’re going to create:

Please note that the slideshow isn’t optimized for mobile devices. The asymmetric/broken layout works best on large screens, so be sure to view the demo from a large device. For mobile devices, you can choose to have a standard slideshow with a single image and an overlay with some text above it.

This isn’t an introductory tutorial. Throughout the years, I’ve published various tutorials where I present GSAP in detail and discuss how to animate the clip-path property. If you aren’t familiar enough with this stuff or need a refresher, I’d encourage you to check the tutorials below before moving forward.

1. Begin with the HTML markup

Inside a container, we’ll place:

  • A list of slides
  • An element that will display the active slide index.
  • The navigation arrows

We’ll assume that each slide will describe a house/apartment for rent and include a title, some images, and a call-to-action button.

The position of these elements will differ and depend on three layout-* classes (layout-a, layout-b, layout-c).

By default, the first slide will be visible thanks to the is-active class. 

Here’s the required markup in general:

1
<div class="slides-wrapper">
2
  <ul class="slides">
3
    <li class="slide layout-a is-active">...</li>
4
    <li class="slide layout-b">...</li>
5
    <li class="slide layout-c">...</li>
6
  </ul>
7
  <div class="counter"><span>1</span> / 3</div>
8
  <div class="arrows-wrapper">
9
    <button class="arrow arrow-prev" aria-label="Navigate to the previous house">...</button>
10
    <button class="arrow arrow-next" aria-label="Navigate to the next house">...</button>
11
  </div>
12
</div>

And more specifically, the markup inside each slide will look like this:

1
<figure class="img-wrapper img1-wrapper">
2
  <img width="" height="" src="IMG_URL" alt=""> 
3
</figure>
4
<figure class="img-wrapper img2-wrapper">
5
  <img width="" height="" src="IMG_URL" alt="">
6
</figure>
7
<figure class="img-wrapper img3-wrapper">
8
  <img width="" height="" src="IMG_URL" alt="">
9
</figure>
10
<figure class="img-wrapper img4-wrapper">
11
  <img width="" height="" src="IMG_URL" alt="">
12
</figure>
13
<h2 class="title text">...</h2>
14
<a href="" class="btn-book text">...</a>

2. Add the CSS

As usual, let’s concentrate on the key styles—we’ll leave the introductory ones for now.

First, we’ll use CSS Grid to stack all the slides; at any time, only the one with the is-active class will appear. 

1
.slides-wrapper .slides {
2
  display: grid;
3
}
4

5
.slides-wrapper .slide {
6
  grid-area: 1/1;
7
  opacity: 0;
8
  visibility: hidden;
9
}
10

11
.slides-wrapper .slide.is-active {
12
  opacity: 1;
13
  visibility: visible;
14
}

Each slide will be a grid container with 20 columns and rows. Plus, each row will have a 5vh height.

The slide gridThe slide gridThe slide grid
1
.slides-wrapper .slide {
2
  display: grid;
3
  grid-template-rows: repeat(20, 5vh);
4
  grid-template-columns: repeat(20, 1fr);
5
}

Next, each slide item will sit in a different location based on its grid-row and grid-column values. In addition, some slides’ items will share the same grid-row-end and grid-column-end values. These are all arbitrary, and you can change them as you wish.

1
.slides-wrapper .layout-a .img1-wrapper {
2
  grid-row: 1 / -1;
3
  grid-column: 1 / span 7;
4
}
5

6
.slides-wrapper .layout-a .img2-wrapper {
7
  grid-row: 6 / span 5;
8
  grid-column: 16 / -1;
9
}
10

11
.slides-wrapper .layout-a .img3-wrapper {
12
  grid-row: 8 / span 9;
13
  grid-column: 10 / span 5;
14
}
15
.slides-wrapper .layout-a .img4-wrapper {
16
  grid-row: 15 / -1;
17
  grid-column: 17 / -1;
18
}
19
.slides-wrapper .layout-a .title {
20
  grid-row-start: 7;
21
  grid-column-start: 1;
22
}
23

24
.slides-wrapper .layout-a .btn-book {
25
  grid-row: 3;
26
  grid-column-start: 11;
27
}

The images will perfectly fit inside their cell thanks to the object-fit: cover super useful CSS property.

1
.slides-wrapper img {
2
  width: 100%;
3
  height: 100%;
4
  object-fit: cover;
5
}

Lastly, the navigation-related items will be absolutely positioned and sit on the left and right edges of the slideshow.

1
.slides-wrapper .counter,
2
.slides-wrapper .arrows-wrapper {
3
  position: absolute;
4
  top: 20px;
5
}
6

7
.slides-wrapper .counter {
8
  left: 20px;
9
}
10

11
.slides-wrapper .arrows-wrapper {
12
  right: 20px;
13
}

3. Add the JavaScript

At this point, we’re ready to add interactivity to our slideshow. 

Each time we click on a navigation arrow, we’ll perform the following actions:

  1. Grab the active slide and its items.
  2. Make sure that all the animated elements of our slideshow become immediately visible by using GSAP’s set() method. We do this to cancel any previous inline styles that are applied during the cycling.
  3. Check to see which button is clicked. If that’s the next button, we’ll set the next active slide as the one that immediately follows the current active slide. If there isn’t such a slide, the next slide becomes the first one. Similarly, if the previous button is clicked, we’ll set the next slide as the one that immediately precedes the current active slide. If there isn’t such a slide, the next slide becomes the last one.
  4. With all this knowledge in place, we’ll call our tl() function where we animate all the slideshow items.

Here’s the required JavaScript code:

1
...
2

3
btnArrows.forEach(function (btn) {
4
  btn.addEventListener("click", function (e) {
5
    // 1
6
    const activeSlide = slidesWrapper.querySelector(".slide.is-active");
7
    const activeSlideImgs = activeSlide.querySelectorAll("img");
8
    const activeSlideText = activeSlide.querySelectorAll(".text");
9
    let nextSlide = null;
10

11
    // 2
12
    gsap.set(slideImgs, { clipPath: "inset(0 0 0 0)" });
13
    gsap.set(slideTexts, { opacity: 1 });
14

15
    // 3
16
    if (e.currentTarget === btnArrowNext) {
17
      nextSlide = activeSlide.nextElementSibling
18
        ? activeSlide.nextElementSibling
19
        : firstSlide;
20
    } else {
21
      nextSlide = activeSlide.previousElementSibling
22
        ? activeSlide.previousElementSibling
23
        : lastSlide;
24
    }
25
    // 4
26
    tl(nextSlide, activeSlide, activeSlideImgs, activeSlideText);
27
  });
28
});

Of course, if we want to be safer, we can wait to run this code when all the page assets load via the load event.

Inside the tl() function we’ll create a GSAP timeline that will hide all the elements of the currently active slide simultaneously. Most importantly, its images will disappear by animating their clip-path property. The interesting thing here is that the animation movement will come from a random clip-path selection. 

As soon as this timeline finishes, we’ll register another timeline that will show the elements of the new active slide again at once. This time though, the associated images will appear with an opposite slide animation. For example, if the previous images are clipped from left to right, these will appear from right to left.

Here’s the signature of this function:

1
function tl(
2
  nextActiveEl,
3
  currentActiveSlide,
4
  currentActiveSlideImgs,
5
  currentSlideActiveText
6
) {
7
  const tl = gsap.timeline({ onComplete });
8

9
  const randomClipPathOption = Math.floor(
10
    Math.random() * clipPathOptions.length
11
  );
12

13
  tl.to(currentActiveSlideImgs, {
14
    clipPath: clipPathOptions[randomClipPathOption]
15
  }).to(
16
    currentSlideActiveText,
17
    {
18
      opacity: 0,
19
      duration: 0.15
20
    },
21
    "-=0.5"
22
  );
23

24
  function onComplete() {
25
    currentActiveSlide.classList.remove(ACTIVE_CLASS);
26
    nextActiveEl.classList.add(ACTIVE_CLASS);
27
    counterSpan.textContent = slidesArray.indexOf(nextActiveEl) + 1;
28
    
29
    const nextSlideImgs = nextActiveEl.querySelectorAll("img");
30
    const nextSlideText = nextActiveEl.querySelectorAll(".text");
31
    const tl = gsap.timeline();
32

33
    tl.from(nextSlideImgs, {
34
      clipPath: clipPathOptions[randomClipPathOption]
35
    }).from(
36
      nextSlideText,
37
      {
38
        opacity: 0,
39
        duration: 0.15
40
      },
41
      "-=0.5"
42
    );
43
  }
44
}

Add keyboard support

Just to enhance the functionality of our slideshow, we’ll add support for keyboard navigation. That said, each time the left () or right () arrow keys are pressed, we’ll trigger a click to the previous and next navigation arrows respectively. 

Here’s the relevant code:

1
document.addEventListener("keyup", (e) => {
2
  console.log(e.key);
3
  if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
4
    // left arrow
5
    if (e.key === "ArrowLeft") {
6
      btnArrowPrev.click();
7
    } else {
8
      // right arrow
9
      btnArrowNext.click();
10
    }
11
  }
12
});

Conclusion

Done! During this tutorial, we’ve been really creative and learned to build an animated GSAP slideshow whose slides consist of different unique asymmetric layouts.

Hopefully, you liked the resulting demo and will use it as inspiration to create your own broken grid JavaScript slideshows 🙏.

As always, thanks a lot for reading!