ActiveList basic carousel tutorial

1. Introduction

In this tutorial we are going to build a Carousel using vanilla JavaScript without using any framework. A carousel is a nice component to build as it encompasses almost everything the ActiveList has to offer.

That we are using vanilla JavaScript which requires us to write our own DOM manipulation inside of a subscriber. When using a reactive framework such as Svelte or Vue, this is not necessary, as these frameworks handle synchronizing with the DOM for us. See Usage with Frameworks for more information.

To get started with the tutorial open and fork this StackBlitz. By using the sandbox you do not have to worry to much about setting up an entire development environment.

2. Files overview

In the sandbox you will see a couple of files:

  1. carousel.css it contains all styling, and the animations for the carousel.
  2. index.html contains the HTML for the carousel, which is currently static, but we will bring it to life. It also includes "uiloos" from the UNPKG cdn.
  3. carousel.js in this empty file we are going to bring the carousel to life.

3. Goals

We want our carousel to do the following things:

  1. It should automatically move to the next slide after a certain delay. With animations based on the direction so it looks nice.
  2. Have previous and next slide buttons so the users can control the carousel manually.
  3. A progress indicator so the user has a sense of the position within the carousel.
  4. A way to directly activate a slide, without having to click the next and previous buttons multiple times.

The slides are contained in a ul HTML element, where each li is a slide. A slide has the following HTML structure:

<li id="carousel-slide-0" class="slide active">
  <img
  width="1920"
  height="1280"
  src="/images/snow.jpg"
  alt="image of a snow owl"
/>
  <article>
    <h4>Snow owl</h4>
    <p>
      The snowy owl (Bubo scandiacus), etc etc
    </p>

    <p>
      Photo by
      <a
        href="https://unsplash.com/@dkphotos"
        target="_blank"
        rel="noopener noreferrer"
      >
        Doug Kelley
      </a>
    </p>
  </article>
</li>

The most important part for us is the class attribute of the li:

<li id="carousel-slide-0" class="slide active">

The CSS class triggers the animations, and determines which slide is visible at the moment. When the slide is given the class active it is the current visible active slide.

To get the carousel to work we must first initialize an ActiveList.

// carousel.js

// First get all slides
const slideEls = Array.from(
  document.querySelectorAll(".slide")
);

// Then to represent the contents of the ActiveList turn the 
// slideEls into an array of numbers. This makes it easier 
// to get the corresponding progress button when a slide is 
// activated.
const slides = slideEls.map((e, index) => index);

/* 
  Because we use the UNPKG variant the ActiveList 
  module is available under the "uiloosActiveList" variable.
*/
const carousel = new window.uiloosActiveList.ActiveList(
  {
    contents: slides,
    activeIndexes: slides[0],
  },
  new window.uiloosActiveList.createActiveListSubscriber({
    // Empty for now
  })
);

The contents of the ActiveListConfig determines how many items there are in the ActiveList. What you put into the contents is your own choice, it can be a complex object, a DOM element reference, or as in our case a simple number.

We choose a number here because each li has a unique id of which is formatted like: carousel-slide-${number}. This allow us to identify the slide and corresponding progress later.

Next lets add an autoPlay so the carousel automatically moves to the next active slide, and also lets flesh out the subscriber to make it work:

const carousel = new window.uiloosActiveList.ActiveList(
  {
    contents: slides,
    activeIndexes: slides[0],
    
    // After 5 seconds go to the next slide
    autoPlay: {
      duration: 5000
    }
  },
  new window.uiloosActiveList.createActiveListSubscriber({
    onActivated(event, carousel) {
      // Get a reference to the activated slide <li>.
      const activatedSlide = document.getElementById(
        `carousel-slide-${carousel.lastActivated}`
      );

      // Make the active slide active.
      activatedSlide.classList.add("active");

      // Get a reference to the deactivated slide <li>.
      const deactivatedSlide = document.getElementById(
        `carousel-slide-${carousel.lastDeactivated}`
      );

      // Deactivate the deactivated slide
      deactivatedSlide.classList.remove("active");
    }
  })
);

The job of the subscriber is to listen to changes in the ActiveList, and synchronize these changes with the DOM. When using vanilla JavaScript you are required to write these DOM manipulations manually, when using a framework such as React, Vue, Svelte etc etc you do not need to do this yourself.

The createActiveListSubscriber allows you to listen to events, in our case "onActivated" which is fired whenever a content of an ActiveList is activated via a ActiveListActivatedEvent.

You might notice that the autoPlay will stop at the last slide, this is because autoPlay will stop when the end of the list is reached. To make things a little nicer we are going to tell the ActiveList that it never ends, by setting isCircular to true.

// carousel.js

const carousel = new window.uiloosActiveList.ActiveList(
  {
    contents: slides,
    activeIndexes: slides[0],
    
    // After 5 seconds go to the next slide
    autoPlay: {
      duration: 5000
    },

    /* 
      This ActiveList is a circle, this means that the autoPlay
      will wrap around back to the beginning when moving past the 
      final slide
    */
    isCircular: true
  },
  // Subscriber is still the same
};

Now that the carousel is spinning in circles, lets fix the progress indicators next, as currently the first slide stays active (purple). Change onActivated to:

onActivated(event, carousel) {
  // Get a reference to the activated slide <li>.
  const activatedSlide = document.getElementById(
    `carousel-slide-${carousel.lastActivated}`
  );

  // Make the active slide active.
  activatedSlide.classList.add("active");

  // Get a reference to the activated slide <li>.
  const deactivatedSlide = document.getElementById(
    `carousel-slide-${carousel.lastDeactivated}`
  );

  // reset the className and set it to slide
  deactivatedSlide.classList.remove("active");

  // Get a reference to the activated slides progress button
  const activatedButton = document.getElementById(
    `carousel-button-${carousel.lastActivated}`
  );

  activatedButton.classList.add("active");

  // Get a reference to the activated slides progress button
  const deactivatedButton = document.getElementById(
    `carousel-button-${carousel.lastDeactivated}`
  );

  deactivatedButton.classList.remove("active");
}

Now that the progress button is working, we can now focus on the next and previous buttons. We need to use the activateNext and activatePrevious methods on the ActiveList. These allow us to activate items based on the item which is currently active:

const carousel = new window.uiloosActiveList.ActiveList(
  // Code stays the same
);

document.getElementById('carousel-next').onclick = () => {
  carousel.activateNext();
};

document.getElementById('carousel-prev').onclick = () => {
  carousel.activatePrevious();
};

We grab the buttons through document.getElementById and call the appropriate method. Note that we do not need to worry about the isCircular here, or which content is currently active etc etc. This is really powerful instead of writing this logic yourself, you let the ActiveList do the heavy lifting.

Next is responding to the progress indicator button clicks:

// Continued below the snippet above

// When progress buttons are clicked activate the slide.
// the button represents.
slides.forEach((slide) => {
  // Remember: slide is a number.

  const progressButton = document.getElementById(
    `carousel-button-${slide}`
  );
  
  progressButton.onclick = () => {
    carousel.activate(slide);
  };
});

An ActiveList can be activated by calling the activate method. The argument you need to pass to it is the value of what you want to activate. Since in our case the contents are numbers (representing the slides) a number will do.

By now we have a pretty nice working carousel, the only thing missing are animations.

5. Animating the slides

For the animation we want the old slide to animate away, and the new slide to animate in. We want the direction of the animation to be based on the position of the old and new slide. If the new slide is on the old slides right we want the old slide to slide out via the left and vice versa.

The trick is setting the correct CSS classes on our slide li, lets change the onActivated again this time to:

onActivated(event, carousel) {
  // Get a reference to the activated slide <li>.
  const activatedSlide = document.getElementById(
    `carousel-slide-${carousel.lastActivated}`
  );

  // Reset the class name to make it easier to reason about.
  activatedSlide.className = "slide";

  // Request animation frame so the animation
  // is less janky.
  requestAnimationFrame(() => {
    // The ActiveList knows which direction it
    // went, by setting it as the CSS class it
    // will move the newly active slide ofscreen
    // to that position.
    activatedSlide.classList.add(carousel.direction);

    // Now that the new active slide is in position
    // animate it so it takes center stage.
    requestAnimationFrame(() => {
      activatedSlide.classList.add("animate", "active");
    });
  });

  // Get a reference to the activated slide <li>.
  const deactivatedSlide = document.getElementById(
    `carousel-slide-${carousel.lastDeactivated}`
  );

  // Reset the class name to make it easier to reason about.
  deactivatedSlide.className = "slide";

  requestAnimationFrame(() => {
    // Make this slide active so it takes
    // center stage.
    deactivatedSlide.classList.add("active");

    // Frame is so that there is no "white"
    // gap between the slides. By sliding the
    // old active slide slightly later there
    // is always an overlap which looks nicer
    // visually.
    requestAnimationFrame(() => {
      // Make it so it is no longer the active slide.
      deactivatedSlide.classList.remove("active");

      // Now move it to the opposite direction
      // of the carousel so it slides out.
      deactivatedSlide.classList.add("animate", carousel.oppositeDirection);
    });
  });

  // reset the className and set it to slide
  deactivatedSlide.classList.remove("active");

  // Get a reference to the activated slides progress button
  const activatedButton = document.getElementById(
    `carousel-button-${carousel.lastActivated}`
  );

  activatedButton.classList.add("active");

  // Get a reference to the activated slides progress button
  const deactivatedButton = document.getElementById(
    `carousel-button-${carousel.lastDeactivated}`
  );

  deactivatedButton.classList.remove("active");
}

As you can see the ActiveList has a property called the direction which keeps track of the way the direction the next active item moves to. It also has a property called oppositeDirection which is always the opposite of the direction property.

We use the direction to decide from which direction the active slide should come from, and the oppositeDirection to determine were the previous slide should go to.

6. Animating progress

It would be nice if the user has an indication of how long a slide is going to take. Lets animate the progress buttons to give the user a sense of time. This time we are going to need an onInitialized event method:

onInitialized(event, carousel) {
  // Trigger the animation for the initial active button,
  // then exit.
  const button = document.getElementById(
    `carousel-button-${carousel.lastActivated}`
  );

  button.classList.add("active");
  button.style.animation = `progress ${carousel.autoPlay.duration}ms linear`;
},

onActivated(event, carousel) {
  // Abbreviated for brevity, everything  up 
  // until this part is the same.

  // Start the animation for the active button.
  const activatedButton = document.getElementById(
    `carousel-button-${carousel.lastActivated}`
  );
  activatedButton.classList.add("active");
  activatedButton.style.animation = `progress ${carousel.autoPlay.duration}ms linear`;

  // Remove the animation from the deactivated button
  const deactivatedButton = document.getElementById(
    `carousel-button-${carousel.lastDeactivated}`
  );
  deactivatedButton.className = "";
  deactivatedButton.style.animation = "";
}

Now the user has a great indication of how long the carousel will stay on the slide!

7. Adding a cooldown

When the user clicks on the next or previous buttons really quickly multiple animations are triggered at once. This looks very jarring so we need to do something about it. Lets add a cooldown:

{
  // Below isCircular:

  // Wait 500 milliseconds before allowing another item
  // to be activated.
  cooldown: 500
}

By setting a cooldown the ActiveList will not allow any activations within that cooldown period.

Any activation event that is triggered will simply be ignored. That is why we do not need to alter our progressButton.onclick to enable the cooldown.

8. Stopping on interaction

Now I want to stop the carousels auto play whenever the user manually changes the active slide. This way the user can assume complete control, luckily for use there is configuration that lets us do this called: stopsOnUserInteraction. When it is set to true the autoPlay stops whenever an interaction came from a user.

{
  // Change the autoPlay config to:
  
  autoPlay: {
    // After 5 seconds go to the next slide
    duration: 5000,

    // If the user clicks a button stop the autoPlay
    stopsOnUserInteraction: true
  },
};

This works because each method on the ActiveList and ActiveListContent is by default assumed to be a user interaction. So when we call carousel.activate(slide); for example it is considered executed by a human.

For more information about how human / computer interactions work see the AutoPlay section on the concepts page.

9. Pause on hover

Next I want the animation to pause whenever the user hovers over the carousel, this way the user can view the images and read the text if they find it interesting.

First thing we need to do is pause the carousel on hover:

// A reference to the carousel <ul> element
const carouselEl = document.getElementById("carousel");

// Disable the carousel when users mouse enters the carousel
carouselEl.addEventListener("mouseenter", () => {
  carousel.pause();
});

// Enable the carousel again when users mouse exits the carousel.
carouselEl.addEventListener("mouseleave", () => {
  // Do not play again if autoPlay was already stopped due to user interaction
  if (!carousel.autoPlay.hasBeenStoppedBefore) {
    carousel.play();
  }
});

The hasBeenStoppedBefore lets you know if the autoPlay was ever completely stopped. This will become true whenever the user takes control over the carousel by using the buttons to activate a slide.

In this scenario I do not want the carousel to start playing again if the user hovers out of the carousel.

Next we need to actually stop the progress button animation when the carousel is paused and resume when playing again. Lets listen to the paused and playing events:

// Add these below or above `onActivated`.

onAutoPlayPaused(event, carousel) {
  // Halt the animation when paused
  const progressButton = document.getElementById(
    `carousel-button-${carousel.lastActivated}`
  );

  progressButton.style.animationPlayState = "paused";
},

onAutoPlayPlaying(event, carousel) {
  // Resume animation when playing
  const progressButton = document.getElementById(
    `carousel-button-${carousel.lastActivated}`
  );

  progressButton.style.animationPlayState = "running";
}

The trick here is using animationPlayState to control a CSS animation from within JavaScript.

10. What you have learned

  1. That the subscriber receives all events that take place on the ActiveList, and that in the subscriber you must sync the DOM with what occurred.
  2. That via autoPlay we can move through the slides programmatically at an interval.
  3. That via autoPlay can be paused and resumed.
  4. We can make an ActiveList not have an end and a beginning by setting the isCircular to true.
  5. That the cooldown property can prevent changes to the active item within a time span, so animations get a chance to play out.
  6. That the ActiveList and ActiveListContent contain methods which can change which content is active.

11. Further reading

  1. Browse through the API for the ActiveList, see everything at your disposal.
  2. Read the API for the ActiveListContent. As it often provides the most convenient API for mutating the ActiveList.
  3. For a more modern CSS snap based carousel take a look at this tutorial: CSS snap carousel tutorial
  4. When using vanilla JavaScript you must handle DOM manipulation yourself, contrast this with the examples that use a framework.

12. Full code

For reference here is the reference implementation of a classic carousel in vanilla JS.

Usage with Frameworks
Learn how to use the ActiveList in combination with frameworks such as React and Vue.