Carousels, slid­ers, and gal­leries come up again and again in front-end web devel­op­ment. We recent­ly put togeth­er a sequence of review videos in an infi­nite­ly scrolling gallery for JDS Labs, a head­phone amp man­u­fac­tur­er and one of our clients. In this arti­cle, I’ll show you how we did it. By the end, we’ll have some­thing like this:

Before we get start­ed, let’s go through our objectives:

Our goal is to main­tain con­ti­nu­ity when click­ing on the for­ward and back­ward but­tons. In order to accom­plish this, we will need to hide two more slides on the left and right sides, in addi­tion to the three slides that are imme­di­ate­ly vis­i­ble in the exam­ple above.

If more slides are need­ed, they should be added to the right side. This is because we are using the :nth-childCSS selec­tor to help style our carousel. There’ll be more about this later.

When click­ing on the pre­vi­ous slide, the last hid­den slide should become the first in the sequence. 

When click­ing on the next slide, the first hid­den slide should go to the last posi­tion of the list.

As a ref­er­ence, here is a schema illus­trat­ing the sequenc­ing of the slides. 

Our struc­ture

The HTML itself is straight­for­ward. We’re going to set the images of our carousel using indi­vid­ual CSS class­es and the background-image prop­er­ty (as opposed to using img tags). This way we can more eas­i­ly con­trol 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 ele­ments are cen­tered 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 atten­tion to the transition prop­er­ty on the .gallery__item. This lit­tle guy will pro­vide the silky smooth tran­si­tion from slide to slide. 

Now, we need to posi­tion each gallery item, con­sid­er­ing its Z axis posi­tion. This ensures that the slides don’t over­lap incor­rect­ly. In order to accom­plish this, we are going to use the :nth-child selec­tor. This selec­tor allows us to specif­i­cal­ly tar­get indi­vid­ual child ele­ments with­in their par­ent — in our case we will tar­get each of our vis­i­ble slides as well as the remain­ing 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, noth­ing dis­plays yet. Let’s cre­ate the back­ground 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 cen­ter, ready for some action.

On to the buttons…

To cre­ate the next and pre­vi­ous but­tons, we will style them exact­ly like the vis­i­ble slides on the left and ride sides of the frame. 

We also need to posi­tion them a lay­er above these slides with a trans­par­ent back­ground. To the user, it appears as if they are click­ing on the slide itself when real­ly they are click­ing 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 pol­ish the appear­ance, let’s add a gra­di­ent to the gallery sides using the pseu­do ele­ments :before and :after

Note:Be aware of the z‑index! We need to posi­tion the gra­di­ents so that the but­tons 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%);
}

Mak­ing it work

The hard part is done. Now, let’s put it all togeth­er. Before adding our code, we want to ensure that all of our HTML is loaded. There are a num­ber of ways of doing this, but let’s use the DOMContentLoad event

document.addEventListener('DOMContentLoaded', function() {
	// Code comes here
});

Then we select the ele­ments 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 but­tons 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');
	});
});

Atten­tion!Notice that we update the items vari­able. This is extreme­ly impor­tant because once we mod­i­fy the item’s posi­tion in the DOM, the vari­able needs to be updat­ed as well. Oth­er­wise we will select the wrong item on the next click.

As you’ve seen, it’s easy to get a gallery up and run­ning. This code does have some lim­i­ta­tions though, like requir­ing at least 5 slides in order to work properly.

There are also improve­ments that could be made, as needed. 

  • Using a lib to lazy load the images not vis­i­ble in the screen
  • Avoid­ing mul­ti­ple, sequen­tial user clicks, allow­ing the slides to fin­ish their tran­si­tion first
  • Adding indi­ca­tors to the slides

How­ev­er, let’s keep this list in mind for a sub­se­quent post. I’m tired!

Until next time. Bye.