Creating an infinite gallery with vanilla JS
In the maiden voyage of Stylin', I'll show you how to build a simple, smooth gallery using just a couple lines of code.
Carousels, sliders, and galleries come up again and again in front-end web development. We recently put together a sequence of review videos in an infinitely scrolling gallery for JDS Labs, a headphone amp manufacturer and one of our clients. In this article, I’ll show you how we did it. By the end, we’ll have something like this:
Before we get started, let’s go through our objectives:
Our goal is to maintain continuity when clicking on the forward and backward buttons. In order to accomplish this, we will need to hide two more slides on the left and right sides, in addition to the three slides that are immediately visible in the example above.
If more slides are needed, they should be added to the right side. This is because we are using the :nth-child
CSS selector to help style our carousel. There’ll be more about this later.
When clicking on the previous slide, the last hidden slide should become the first in the sequence.
When clicking on the next slide, the first hidden slide should go to the last position of the list.
As a reference, here is a schema illustrating the sequencing of the slides.
Our structure
The HTML itself is straightforward. We’re going to set the images of our carousel using individual CSS classes and the background-image
property (as opposed to using img
tags). This way we can more easily control the aspect ratio of the images.
<div class="gallery">
<div class="gallery__prev"></div>
<div class="gallery__next"></div>
<div class="gallery__stream">
<div class="gallery__item bg-1"></div>
<div class="gallery__item bg-2"></div>
<div class="gallery__item bg-3"></div>
<div class="gallery__item bg-4"></div>
<div class="gallery__item bg-5"></div>
<div class="gallery__item bg-6"></div>
<div class="gallery__item bg-7"></div>
</div>
</div>
Stylin’
First, let’s style the gallery and its items. Notice that the elements are centered vertically.
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #000;
}
/* Gallery */
.gallery {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 100%;
height: 90%;
max-height: 28vw;
overflow: hidden;
}
.gallery__stream {
position: relative;
top: 50%;
transform: translateY(-50%);
width: 100%;
height: 100%;
}
.gallery__item {
position: absolute;
width: 50%;
height: 100%;
transition: 1s all ease;
background-size: cover;
background-repeat: no-repeat;
background-position: center center;
border-radius: 5px;
}
Pay attention to the transition
property on the .gallery__item
. This little guy will provide the silky smooth transition from slide to slide.
Now, we need to position each gallery item, considering its Z axis position. This ensures that the slides don’t overlap incorrectly. In order to accomplish this, we are going to use the :nth-child
selector. This selector allows us to specifically target individual child elements within their parent — in our case we will target each of our visible slides as well as the remaining “off-screen” slides.
.gallery__item:nth-child(1) {
left: 0;
z-index: 1;
transform: translateX(-100%) scale(.8);
}
.gallery__item:nth-child(2) {
left: 0;
z-index: 2;
transform: translateX(-50%) scale(.8);
}
.gallery__item:nth-child(3) {
left: 50%;
z-index: 4;
transform: translateX(-50%) scale(1);
}
.gallery__item:nth-child(4) {
left: 100%;
z-index: 2;
transform: translateX(-50%) scale(.8);
}
.gallery__item:nth-child(n+5) {
left: 100%;
z-index: 1;
transform: scale(.8);
}
Since we haven’t added the images, nothing displays yet. Let’s create the background classes:
/* Backgrounds */
.bg-1 {
background-image: url(https://res.cloudinary.com/dm7h7e8xj/image/upload/v1554487589/star-wars-1_kgt8dr.jpg);
}
.bg-2 {
background-image: url(https://res.cloudinary.com/dm7h7e8xj/image/upload/v1554487590/star-wars-2_tgzyxe.jpg);
}
.bg-3 {
background-image: url(https://res.cloudinary.com/dm7h7e8xj/image/upload/v1554487591/star-wars-3_bkcmeb.jpg);
}
.bg-4 {
background-image: url(https://res.cloudinary.com/dm7h7e8xj/image/upload/v1554487591/star-wars-4_opgyza.jpg);
}
.bg-5 {
background-image: url(https://res.cloudinary.com/dm7h7e8xj/image/upload/v1554487591/star-wars-5_ulc9tx.jpg);
}
.bg-6 {
background-image: url(https://res.cloudinary.com/dm7h7e8xj/image/upload/v1554487591/star-wars-6_la8whc.jpg);
}
.bg-7 {
background-image: url(https://res.cloudinary.com/dm7h7e8xj/image/upload/v1554487590/star-wars-7_l3fcor.jpg);
}
Neat! Anakin is front and center, ready for some action.
On to the buttons…
To create the next and previous buttons, we will style them exactly like the visible slides on the left and ride sides of the frame.
We also need to position them a layer above these slides with a transparent background. To the user, it appears as if they are clicking on the slide itself when really they are clicking the buttons.
.gallery__prev,
.gallery__next {
position: absolute;
top: 50%;
z-index: 4;
width: 50%;
height: 100%;
transform: translateX(-50%) translateY(-50%) scale(.8);
cursor: pointer;
}
.gallery__prev {
left: 0;
}
.gallery__next {
left: 100%;
}
To polish the appearance, let’s add a gradient to the gallery sides using the pseudo elements :before
and :after
Note:Be aware of the z‑index! We need to position the gradients so that the buttons remain clickable.
.gallery:before,
.gallery:after {
display: block;
content: "";
position: absolute;
top: 0;
width: 20%;
height: 100%;
z-index: 3;
}
.gallery:before {
left: 0;
background: linear-gradient(to right, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 100%);
}
.gallery:after {
right: 0;
background: linear-gradient(to left, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 100%);
}
Making it work
The hard part is done. Now, let’s put it all together. Before adding our code, we want to ensure that all of our HTML is loaded. There are a number of ways of doing this, but let’s use the DOMContentLoad
event
document.addEventListener('DOMContentLoaded', function() {
// Code comes here
});
Then we select the elements we need:
document.addEventListener('DOMContentLoaded', function() {
var stream = document.querySelector('.gallery__stream');
var items = document.querySelectorAll('.gallery__item');
var prev = document.querySelector('.gallery__prev');
var next = document.querySelector('.gallery__next');
});
And bind the click events to the buttons prev and next:
document.addEventListener('DOMContentLoaded', function() {
var stream = document.querySelector('.gallery__stream');
var items = document.querySelectorAll('.gallery__item');
var prev = document.querySelector('.gallery__prev');
var next = document.querySelector('.gallery__next');
prev.addEventListener('click', function() {
stream.insertBefore(items[items.length - 1], items[0]);
items = document.querySelectorAll('.gallery__item');
});
next.addEventListener('click', function() {
stream.appendChild(items[0]);
items = document.querySelectorAll('.gallery__item');
});
});
Attention!Notice that we update the items
variable. This is extremely important because once we modify the item’s position in the DOM, the variable needs to be updated as well. Otherwise we will select the wrong item on the next click.
As you’ve seen, it’s easy to get a gallery up and running. This code does have some limitations though, like requiring at least 5 slides in order to work properly.
There are also improvements that could be made, as needed.
- Using a lib to lazy load the images not visible in the screen
- Avoiding multiple, sequential user clicks, allowing the slides to finish their transition first
- Adding indicators to the slides
However, let’s keep this list in mind for a subsequent post. I’m tired!
Until next time. Bye.