Carousels pt. - Building a JS carousel from the ground up

Published

I recently built a website for my friend Kevin to promote his skills, one of them being photography. He asked for an image carousel to show off some of his photos—so, instead of picking up one of the thousands of existing image carousel libraries, I decided to roll my own. You know, for kicks.

Check the carousel in action right here.

Since my last post about carousels got lot of attention, I decided to write this post you now find yourself reading.

I wanted to keep the markup dead simple because, when I was starting out with web dev, I actually found a lot of the carousel libraries out there to be overly complicated, some with really weird markup requirements. So, after some thought, I came up with the following structure for the markup:

1<div id="fancy-carousel">
2  <img src="./images/01.jpg" alt="Alt text goes here :-)" />
3  <img src="./images/02.jpg" alt="Alt text goes here :-o" />
4  <img src="./images/03.jpg" alt="Alt text goes here ;-p" />
5</div>
6

So, basically, I have a div with the id fancy-carousel, I put images in it, and the JavaScript does all the work for me. No data attributes or complicated markup structure. After everything is put together, it looks something like this:

See the Pen Fancy Carousel by Daniel Cortes (@dgca) on CodePen.


So what’s happening is the JS finds our div#fancy-carousel, creates two new divs, each stacked on top of each other, and uses the top (as in z-order) one to house the controls—the previous and next arrows.

The div underneath (let’s call it the ‘display div’) is responsible for displaying the current image. I created an array out of the image sources, which is then applied as the display div’s background image. When the next arrow is clicked, the next image source in the array is set as the display div’s background. Clicking the previous arrow sets the background image to the previous item in the array.

One thing I wanna point out is that a fancy-carousel–active class is being added to the main carousel wrapper, and a hide class is being added to the img elements as they go through the for loop that creates the array of image sources. This is so we can conditionally style the carousel if the JS runs. If you open up the Codepen example on the Codepen site and delete all the JS code, you’ll see that the images render how they would normally render on the page.

So here’s the final JS code, with comments to explain what’s going on.

1var fancyCarousel = document.getElementById("fancy-carousel");
2if (fancyCarousel && fancyCarousel.hasChildNodes()) {
3  // Create the divs that we'll be adding to the DOM to create our markup structure
4  // imagesWrapper will display the image via its backgroundImage CSS property
5  // controlsWrapper will contain the previous and next arrows
6  var imagesWrapper = document.createElement("div");
7  var controlsWrapper = document.createElement("div");
8  var prevArrow = document.createElement("div");
9  var nextArrow = document.createElement("div");
10  // Add classes to our markup so we can style it later
11  fancyCarousel.classList.add("fancy-carousel--active");
12  imagesWrapper.classList.add("fancy-carousel__images-wrapper");
13  controlsWrapper.classList.add("fancy-carousel__controls-wrapper");
14  prevArrow.classList.add("fancy-carousel__prev-arrow");
15  nextArrow.classList.add("fancy-carousel__next-arrow");
16  // Add the previous and next arrow divs to the controlsWrapper
17  controlsWrapper.appendChild(prevArrow);
18  controlsWrapper.appendChild(nextArrow);
19  // Add the imagesWrapper and controlsWrapper to the carousel wrapper
20  fancyCarousel.appendChild(imagesWrapper);
21  fancyCarousel.appendChild(controlsWrapper);
22
23  // Create an array out of the source attribute of any images in the carousel
24  // Add a 'hide' class to the image as they're being iterated on
25  var childNodes = fancyCarousel.childNodes;
26  var imagesArray = [];
27  for (var i = 0; i < childNodes.length; i++) {
28    var currentNode = childNodes[i];
29    if (currentNode.nodeName === "IMG") {
30      imagesArray.push(currentNode.src);
31      currentNode.classList.add("hide");
32    }
33  }
34
35  // Set the imagesWrapper's background image to the first image in the imagesArray
36  imagesWrapper.style.backgroundImage = `url(${imagesArray[0]})`;
37
38  // Here we begin setting up the logic for transitioning between images
39  // First step, set up some variables for the current image index,
40  // and the number of images in the images array
41  var currentImage = 0;
42  var numberOfImages = imagesArray.length;
43
44  // Set up event listeners to call the handleSlideshowArrow() function
45  // when the arrows are clicked
46  prevArrow.addEventListener("click", function () {
47    handleSlideshowArrow("prev");
48  });
49  nextArrow.addEventListener("click", function () {
50    handleSlideshowArrow("next");
51  });
52
53  // Let's use those currentImage and numberOfImages variables from up there
54  // If 'prev' arrow is clicked, decrease currentImage by one,
55  // else if 'next' arrow is clicked increase currentImage by one
56  // ...plus some extra logic so that we don't go into the negatives
57  // or get to an index that doesn't exist
58  function handleSlideshowArrow(val) {
59    if (val === "prev") {
60      if (currentImage > 0) {
61        currentImage--;
62      } else {
63        currentImage = numberOfImages - 1;
64      }
65    } else if (val === "next") {
66      if (currentImage < numberOfImages - 1) {
67        currentImage++;
68      } else {
69        currentImage = 0;
70      }
71    }
72    // Alright, so we're not actually going to be going to the next image
73    // in this function, but we're going to add a 'fade-out' class to our
74    // imagesWrapper so that we can have a nice fade transition from item
75    // to item
76    imagesWrapper.classList.add("fade-out");
77    // And we're going to add an event listener that's going to do the
78    // transitioning as soon as the fade out transition ends, so let's go
79    // into that function below
80    imagesWrapper.addEventListener("transitionend", handleTransitionEnd);
81  }
82
83  function handleTransitionEnd() {
84    // So since our fade out transition has ended, we can remove the event listener
85    imagesWrapper.removeEventListener("transitionend", handleTransitionEnd);
86    // We set the background image to the current image in the imagesArray
87    imagesWrapper.style.backgroundImage =
88      "url(" + imagesArray[currentImage] + ")";
89    // And we remove the fade-out class, which is gonna make our image fade back in
90    imagesWrapper.classList.remove("fade-out");
91  }
92}
93// And there we go, that's it for the JS
94// Next up, the SCSS
95
1// Since we're going to be stacking divs on top of each other, we have
2// to make the main wrapper position relative
3.fancy-carousel--active {
4  background-color: #49708a;
5  height: 300px;
6  overflow: hidden;
7  position: relative;
8
9  // And those images that we added a 'hide' class to—well, we're not gonna
10  // totally hide them with 'display: none' or 'visibility: hidden', because
11  // we still want screen readers to know about them
12  .hide {
13    left: 100%;
14    position: absolute;
15    top: 100%;
16  }
17}
18
19// Stack the images and controls wrapper, and make
20// them the same size as the main container
21.fancy-carousel__images-wrapper,
22.fancy-carousel__controls-wrapper {
23  bottom: 0;
24  left: 0;
25  position: absolute;
26  right: 0;
27  top: 0;
28}
29
30// Let's center those images in the images wrapper,
31// and set our fade out transition which is what the
32// event handler listens to so that it switches out
33// the images when the image is fully transparent
34.fancy-carousel__images-wrapper {
35  background-position: 50% 50%;
36  background-size: cover;
37  opacity: 1;
38  transition: opacity 0.25s;
39
40  &.fade-out {
41    opacity: 0;
42  }
43}
44
45// Here we set up the base styles of the previous and next arrows
46.fancy-carousel__prev-arrow,
47.fancy-carousel__next-arrow {
48  background-color: black;
49  border-radius: 50%;
50  cursor: pointer;
51  height: 30px;
52  opacity: 0.35;
53  position: absolute;
54  top: 50%;
55  transition: opacity 0.25s;
56  transform: translateY(-50%);
57  width: 30px;
58
59  &:hover {
60    opacity: 0.55;
61  }
62
63  // And we make the arrows themselves out of pseudoelements
64  &::before {
65    border-bottom: 3px solid white;
66    border-left: 3px solid white;
67    content: "";
68    display: block;
69    height: 10px;
70    left: 50%;
71    position: absolute;
72    top: 50%;
73    width: 10px;
74  }
75}
76
77// Position the arrows
78.fancy-carousel__prev-arrow {
79  left: 5px;
80
81  &::before {
82    transform: translateY(-50%) translateX(-30%) rotate(45deg);
83  }
84}
85
86.fancy-carousel__next-arrow {
87  right: 5px;
88
89  &::before {
90    transform: translateY(-50%) translateX(-60%) rotate(225deg);
91  }
92}
93// And that's it!
94

So yeah, there you go. HTML, SCSS, and vanilla JS carousel! I’m pretty happy with how this turned out. It works super well on mobile as well as desktop, gracefully degrades if someone doesn’t have JS turned on for whatever reason, and is super easy to integrate.

That said, there are three things I want to do to improve this. For one, we don’t need to load all the images right away. Ideally, the images would load as they’re needed. Secondly, I would like to add swipe support so that you don’t have to tap the arrows on touchscreens. Finally, I’d like to make the whole thing class-based instead of ID based. As is, this wouldn’t work if you needed more than one carousel on screen at a time.

Well, that’s it for this post! If you read this far, thanks for sticking with this and I hope you learned something from this write-up.