The other day, a friend at work showed me a project one of her students had made. It was a web page with multiple image carousels on it and it wasn't working. Now let me just say one quick thing.

I think image carousels are a UX/UI/accessibility mess.

But, they're also a good way to test some basic understanding of

  • DOM manipulation
  • Style
  • Event listeners
  • Logic

So I kind of get why people build them at least for demo purposes.

It took a while for us to debug the issues mostly because the code was pretty hard to follow. It was based on the multiple slideshows example on w3schools...

Anyway, I decided to give myself a challenge to see if I could come up with something better in an hour or less. While my code isn't 'perfect' (I didn't optimize for accessibility for instance), I think it's more maintainable and flexible. So here it is!

Set up

This is what the directory structure looks like

/my-carousel-challenge
  \_ index.html
  \_ index.js
  \_ style.css
  \_ /images
      \_ image-0.jpg
      \_ image-1.jpg
      \_ image-2.jpg

The images might in a real app be fallbacks but here serve as both fallbacks and mock data.

HTML

The challenge the student faced was to make three carousels that would operate independently. So I structured the html like this:

<body>
  <section>
    <div id="carousel-0" class="carousel-container">
      <div id="carousel-0-image" class="carousel-image" style="background-image: url(images/image-0.jpg)"></div>
      <button class="decrement"><</button>
      <button class="increment">></button>
    </div>
    <div id="carousel-1" class="carousel-container">
      <div id="carousel-1-image" class="carousel-image" style="background-image: url(images/image-0.jpg)"></div>
      <button class="decrement"><</button>
      <button class="increment">></button>
    </div>
    <div id="carousel-2" class="carousel-container">
      <div id="carousel-2-image" class="carousel-image" style="background-image: url(images/image-0.jpg)"></div>
      <button class="decrement"><</button>
      <button class="increment">></button>
    </div>
  </section>
</body>

Each carousel has

  • a hardcoded fallback
  • increment and decrement buttons
  • an id for easy access

CSS

In order to keep to my time limit, I kept the styling very minimal.

section {
  max-width: 45em;
  margin: 0 auto;
}

button {
  margin-top: 1em;
  padding: 1em 2em;
}

.carousel-container {
  height: 20em;
}

.carousel-container + .carousel-container {
  margin-top: 4em;
}

.carousel-image {
  background-position: center;
  background-repeat: no-repeat;
  height: 100%;
}

Just enough to

  • display the images and
  • make them a little separated from each other
  • help make the buttons easier to click

javascript

Rather than follow the w3schools approach, I decided I wanted to make sure the code could handle

  • non-hardcoded images (like data from an API)
  • easily accommodating different numbers of carousels

Mock Data

First I mocked out some data like this:

// mock data
const images = [
  {
    url: 'images/image-0.jpg'
  },
  {
    url: 'images/image-1.jpg'
  },
  {
    url: 'images/image-2.jpg'
  }
]

In trying to move as fast as possible, I used the same images as the fallbacks. Obviously this wouldn't be the case in a more realistic app. But, this would simulate the response of an API of some kind.

Constructor function

Next, I wanted to avoid the way w3schools (and the student) hardcoded calling a function to make the carousels work. So, I thought that a constructor function called Carousel would work better. That way I could create a new Carousel() as often as needed (more on that in a minute). For the record, I chose a constructor over a class fo no particular reason this time. It would be equally easy to set it up as a class. This is what I came up with.

const Carousel = function(id, images) {
  
  this.state = {
    currentImageNumber: 0,
    images // the mock data
  }

  // the id of the carousel container
  this.id = id;

  // buttons
  this.decrementButton = document.querySelector(`#${this.id} .decrement`);
  this.incrementButton = document.querySelector(`#${this.id} .increment`);

  // click handlers
  this.decrementButton.onclick = () => {
    // decrement the current image number by 1
    this.state.currentImageNumber--;
    
    // if the result is less than 0
    if (this.state.currentImageNumber < 0) {
      
      // set the current image number to 1 less than the number of images
      this.state.currentImageNumber = this.state.images.length - 1;
    }
    
    // change the image
    this.setCurrentImage();
  }
  
  this.incrementButton.onclick = () => {
    // increment the current number by 1
    this.state.currentImageNumber++;
    
    // if the current number is greater than 1 less than the number of images
    if (this.state.currentImageNumber > this.state.images.length - 1) {
      // reset the current number to 0
      this.state.currentImageNumber = 0;
    }
    
    // change the image
    this.setCurrentImage();
  }

  // set the current image according to the state of the current image
  this.setCurrentImage = () => {
    // get the div where the image is set via css
    const div = document.getElementById(`${this.id}-image`);
    
    // get the url of the image from the mock data
    const bgImage = this.state.images[this.state.currentImageNumber]['url']
    
    // set the style of the div to the url
    div.style.backgroundImage = `url('${bgImage}')`;
  }
}

This way I could easily keep track of which image I wanted at any given time.

Creating carousels

But at this point I might still fall into the trap of hardcoding something like

const carousel0 = new Carousel('carousel-0', images);
const carousel1 = new Carousel('carousel-1', images);
const carousel2 = new Carousel('carousel-2', images);
// etc...

That's obviously no good because every time I need to add or remove a carousel, I have to do it in the html and the js.

Instead I prefer this approach:

// get all the containers regardless of how many there are
const carousels = document.querySelectorAll('.carousel-container');

// create a new Carousel constructor for each one.
carousels.forEach((carousel) => {
  const c = new Carousel(carousel.id, images);
})

The querySelectorAll method takes a css selector as an argument and returns a NodeList which is an iterable (meaning you can loop over it unlike an HTMLCollection). While the forEach method doesn't work in IE there are ways around that, but IE compatibility wasn't high on my list of objectives for this particular exercise.

Next, for every carousel in the NodeList (no matter how many there are), I automatically create a new Carousel with the id and the data.

Conclusion

This was a fun little project to work on quickly. If I had to do it over again, I'd change a few things

  • The styling is terrible
  • There are no transitions between showing and hiding images
  • It fails terribly for accessibility (no alt tags on images for instance)

So, maybe I'll come back and fix it up a bit so that it's more representative of a real component.

In the meantime, don't use carousels and I'll see you next time!