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.
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.
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:
- Grab the active slide and its items.
- 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.
- 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.
- 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!