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.
Inside a container, we’ll place:
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 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
And more specifically, the markup inside each slide will look like this:
1 |
|
2 |
width="" height="" src="IMG_URL" alt="">
|
3 |
|
4 |
|
5 |
width="" height="" src="IMG_URL" alt="">
|
6 |
|
7 |
|
8 |
width="" height="" src="IMG_URL" alt="">
|
9 |
|
10 |
|
11 |
width="" height="" src="IMG_URL" alt="">
|
12 |
|
13 |
|
14 |
href="" class="btn-book text">...
|
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 |
}
|
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:
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 |
}
|
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 |
});
|
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!