DEV Community

Cover image for How to make a horizontal moving carousel with Vanilla JS & TS
min11benja
min11benja

Posted on

How to make a horizontal moving carousel with Vanilla JS & TS

**

PROBLEM

**
There is no doubt that carousels are essential components in a website’s design. When you are limited in space but still want to display a lot of information, carousels come in very handy as they are perfect for displaying groups of related or unrelated content in a limited width size.

There are so many Carousel libraries out there with so much functionality but when looking for a simple version of one that actually moves the cards horizontally I could not find any. Most of them just overlap them one on top of the other or toggle their visibility on and off.

  • Swiper
  • Owl Carousel 2
  • Slick Slider
  • ItemSlider

Not to mention that all of them use jQuery, I'm not going to go too deep into why we should avoid libraries like jQuery, everything JQuery can do, VanillaJS can do better and faster. In the past when we needed to update a jQuery library it almost always breaks something. So for small stuff like this I rather just work with plain javascript code, no libraries.

**

MY APPROACH

**
So I decided to make my own from scratch. Here is how I did it. I first made a list of requirements.

  • It needs navigation arrows to move one card left or right at a time.
  • It also needs dots for navigation to move more than one dot at a time.
  • The cards need to be horizontal on desktop and vertical on mobile.
  • The cards need a squared image on the left and a title, subtitle and a link on the right hand side

Then I got help from Tom, our TI designer, to make a quick mockup on how it should look.

On desktop

Image description

On mobile

Image description

**

Creating the HTML markup for the carousel

**
We will start by making the HTML markup structure of the carousel.
A main container DIV with a class of carousel-wrapper
A wrapper div for each slide and the navigation arrows with a class called carousel
Four slides with placeholder images and Lorem ipsum title, subtitle and text for the link with a class of carousel_card
The first of the slides with an extra class of active
A wrapper div for the each arrow buttons elements for left and right navigation with a class carousel
button--next and carousel_button--prev
Another wrapper div for the dots for each slide

Starting with the carousel-wrapper DIV, we use this to size our carousel and hide any overflow content from inside it. The carousel DIV will hold all of our elements like each slide card and both navigation arrow icons acting as buttons.

Then the DIV witt carousel__card will hold the card image, title, subtitle and link. We will add two more divs with separate unique id with the next and prev flag so we can give each of them their own click event.

The DIV for carousel__dots will hold the list of dots that will act as a second navigation for more than one carousel slide transition. One of them will contain the class active that will look different from the rest.

<div class="carousel-wrapper">
    <div class="carousel">
        <div class="carousel__card active">
            <div>
                <img alt="blog-image-1" src="https://dummyimage.com/142x142/cccccc/999999"/>
            </div>
            <div>
                <p>Fans Love These 5 Features of the TI-84 Plus CE Graphing Calculator</p>
                <br />
                <a target="_blank" alt="Link" class="cta-arrows" href="#">Read more</a>
            </div>
        </div><!--.carousel__card-->
        <div class="carousel__card ">
            <div>
                <img alt="blog-image-2" src="https://dummyimage.com/142x142/cccccc/999999"/>
            </div>
            <div>
                <p>Fans Love These 5 Features of the TI-84 Plus CE Graphing Calculator</p>
                <br />
                <a target="_blank" alt="Link" class="cta-arrows" href="#">Read more</a>
            </div>
        </div><!--.carousel__card-->
        <div class="carousel__card ">
            <div>
                <img alt="blog-image-3" src="https://dummyimage.com/142x142/cccccc/999999"/>
            </div>
            <div>
                <p>Fans Love These 5 Features of the TI-84 Plus CE Graphing Calculator</p>
                <br />
                <a target="_blank" alt="Link" class="cta-arrows" href="#">Read more</a>
            </div>
        </div><!--.carousel__card-->


        <div id="next" class="carousel__button--next"></div>
      <div id="prev" class="carousel__button--prev"></div>

    </div><!--.carousel-->
      <div class="carousel__dots">
        <ul id="dotList">
          <li class="dot active"></li>
          <li class="dot"></li>
          <li class="dot"></li>
        </ul>
      </div><!--.carousel__dots-->
</div><!--.carousel-wrapper-->

Enter fullscreen mode Exit fullscreen mode

**

Creating the CSS styles for the carousel

**
Ok so with this we have our HTML done. Up next, the CSS.
First we will add a font family called Lato from the google fonts page, we will add the normal and bold 700 variations. We will make the Title H3 tag Lato bold and the rest of the text Lato normal.

When navigating through the carousels, we’ll want JS to dynamically set classes to pre-position the prev and next item cards by translating them with the transform property.

Diving in with .carousel-wrapper, nothing special here, just overflow, and width. The idea here is that the width can be changed to suit your content and everything inside it will scale to fit. We will add some margin to the left and right hand side and for the inside padding, for proper spacing. We’ll also want to apply border-box to the box-sizing property so any padding and border are included in our elements’ total width and height.

So to make the cards not be stacked like a hamburger but placed horizontally we will add the display property of FLEx to the .carousel div. We will add the justify and align items property to the center to get the cards aligned correctly.

Now for the carousel__card we will make it have a white background color and add the property of grid and make it two columns with the property of grid-template-columns: auto 300px;

We’ll be using the transform property to move our carousel’s items, so setting the transform-style to preserve-3d will make sure our nested elements are rendered properly in 3D space.
The CSS for the carousel is done, all that’s left is the navigation buttons which will sit in the middle, either side of the carousel with arrows inside. Instead of adding more HTML, we’ll add the arrows using the ::after pseudo element.

Now we will style the dots so they are horizontal and greyed out except for the one with the class active

.section-wrapper {
  background-color:#ebebeb;
  padding:30px;
  margin-top:10rem;
}
h3 {
  font-size: 34px;
  color: #525252;
  text-align: center;
  font-family: "Lato", sans-serif;
  font-weigth: 700;
}
.carousel-wrapper {
  font-family: "Lato", sans-serif;
  overflow: hidden;
  width: 90%;    
  margin-left: 30px;
  margin-right: 30px;
}
.carousel-wrapper * {
  box-sizing: border-box;
}
.carousel {
    border: solid 3px rgba(0,0,0,.25);
    display: flex;
    justify-content: flex-start;
    align-items: center;
}
.carousel {
  transform-style: preserve-3d;
}
.carousel__card {
  opacity: 0.4;
}
.carousel__card.active {
  opacity: 1;
}
.carousel__card {
  background-color: #fff;
  padding: 20px;
  margin-right: 10px;
  border: #fff solid 5px;
  display: grid;
  grid-template-columns: auto 200px;
  grid-gap: 30px;
  align-items: center;
}
.carousel__card {
  transition: transform 0.5s, opacity 0.5s, z-index 0.5s;
}
.carousel__button--prev, .carousel__button--next {
    position: absolute;
    top: 50%;
    width: 3rem;
    height: 3rem;
    background-color: rgb(0 0 0 / 24%);
    transform: translateY(-50%);
    border-radius: 50%;
    cursor: pointer;
    z-index: 1001;        
}
.carousel__button--prev {
  left: 0;
}
.carousel__button--next {
  right: 0;
}
.carousel__button--prev::after,
.carousel__button--next::after {
  content: " ";
  position: absolute;
  width: 10px;
  height: 10px;
  top: 50%;
  left: 54%;
  border-right: 2px solid #fff;
  border-bottom: 2px solid #fff;
  transform: translate(-50%, -50%) rotate(135deg);
}
.carousel__button--next::after {
  left: 47%;
  transform: translate(-50%, -50%) rotate(-45deg);
}
li.dot {
  height: 10px;
  width: 10px;
  background-color: #bbb;
  border-radius: 50%;
  display: inline-block;
  margin: 0px 5px;
}
ul#dotList {
  margin-top: 20px;
  text-align: center;
}
li.dot.active {
  background-color: #525252;
}

Enter fullscreen mode Exit fullscreen mode

So now you should have something like this.

Image description

**

Creating the JS functionality for the carousel

**

To begin, we’ll declare our variables. We need to grab all of our card items and place them inside an array. Then get the array length and save it in a separate variable for later. We will also need a variable to know the current slide and another boolean variable to know if the slide is moving or not.

/*variables*/
const itemClassName = "carousel__card";
let items = document.getElementsByClassName(itemClassName);
let totalItems = items.length;
let activeSlide = 0;
let moving = false;
Enter fullscreen mode Exit fullscreen mode

If you console log these values you should get the following

We first need to add an event listener to the arrow icons, so we can move all of the cards horizontally, either to the right or left depending on wich one is clicked.

// Set event listeners
const nextbtn = document.getElementById("next");
const prevbtn = document.getElementById("prev");
nextbtn.onclick = function () {
  console.log("next button clicked");
};
prevbtn.onclick = function () {
  console.log("prev button clicked");
};
Enter fullscreen mode Exit fullscreen mode

Now when you click on these arrows you should get a console log

To move the cards we will use the transform css property to move them to the left with negative pixels or to the left and positive pixels to the right.

items[0].style.transform = `translate(-400px)`;//move card to the right
items[0].style.transform = `translate(400px)`;//move card to the left
Enter fullscreen mode Exit fullscreen mode

Now we need to do this for each card so we will add this statement inside a for each loop under each event listener for each arrow.

nextbtn.onclick = function () {
  console.log("next button clicked");
  moveCardsHorizontally(-400);
};
prevbtn.onclick = function () {
  console.log("prev button clicked");
  moveCardsHorizontally(400);
};
function moveCardsHorizontally(amountToMove) {
  for (let key in items) {
    if (items.hasOwnProperty(key)) {
      value = items[key];
      console.log(`items[${key}] = ${value}`);
      items[key].style.transform = `translate(${amountToMove}px)`;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Ideally we need to get this amount depending on the amount of cards. We will need to get the containers total width and divide that by the amount of slide to get the amount of pixels we will need to move each card.

We will add a new function called getNewPosition with the parameter of direction. Here we will calculate the total width of the items array parent container and save it to the totalWidth variable. Then we will divide that by the amount of slides in the items array and save it to the singleamountToMovehorizontally variable. We need to check if the direction is to the right or left, because to the right we need the amount of pixels to be the total of multiplying the number of the active slide by the singleamountToMovehorizontally, and if it's from the left we need to take the current positionX and subtract one singleamountToMovehorizontally. I decided to handle this logic of left or right direction in the onclick events below. So if the next button is triggered it's to the right and if the prev button is triggered the direction is to the left. I also added some logic to test if we were on the start or end of the slides so as to not trigger the move moveCardsHorizontally() function if that is the case.

function getNewPosition(direction){
    totalWidth = items[0].parentElement.parentElement.parentElement.clientWidth; //1188
    singleAmountToMoveHorizontally = Math.floor(totalWidth / totalItems);//292  
    console.log(`singleAmountToMoveHorizontally = totalWidth:${totalWidth} / totalItems:${totalItems} = ${singleAmountToMoveHorizontally}`);

    if(direction === "right"){
        console.log("parameter ->move right");                    
        positionX = Math.floor(singleAmountToMoveHorizontally * activeSlide);//
        console.log(`positionX =  singleAmountToMoveHorizontally:${singleAmountToMoveHorizontally} * activeSlide:${activeSlide} = ${positionX}`);  
    }else if(direction === "left"){
        console.log("parameter <- move left");
        //We need to add one because the array index is 0 to n            
        positionX = Math.floor(singleAmountToMoveHorizontally * activeSlide+1);//
        console.log(`positionX = positionX - singleAmountToMoveHorizontally:${singleAmountToMoveHorizontally} = ${positionX}`);  
    }    
  return positionX;
}
nextbtn.onclick = function () {
    console.log("next button clicked");  
//total 5 but array starts at 0 so 4
    if(activeSlide === totalItems-1){
      console.log(`active slide is activeSlide:${activeSlide} = totalItems:${totalItems} can't go more to the right`);  
    }else{
      moveCardsHorizontally(-getNewPosition("right"));  
      activeSlide = activeSlide + 1;
      console.log(`activeSlide + 1 = ${activeSlide}`);
    }        
};
prevbtn.onclick = function () {
        console.log("prev button clicked");  
        if(activeSlide === 0){
          console.log("active slide is 0 cant go more to the left");  
        }else{
          moveCardsHorizontally(-getNewPosition("left"));
          activeSlide = activeSlide-1;
          console.log(`activeSlide - 1 = ${activeSlide}`);         
    }  
};

Enter fullscreen mode Exit fullscreen mode

To avoid people over-clicking the buttons, we’ll disable interactivity while the carousel is animating, and enable it again once it’s finished. I added this function called disableInteraction() and an if statement to check this boolean inside the event listeners.

function disableInteraction() {
    // Set 'moving' to true for the same duration as our transition.
    // (0.5s = 500ms)    
    moving = true;
    // setTimeout runs its function once after the given time
    setTimeout(function(){
      moving = false
    }, 500);
  }
nextbtn.onclick = function () {
    // Check if carousel is moving, if not, allow interaction
  if(!moving) {
    console.log("next button clicked");  
    // temporarily disable interactivity
    disableInteraction();
    //total 5 but array starts at 0 so 4
    if(activeSlide === totalItems-1){
      console.log(`active slide is activeSlide:${activeSlide} = totalItems:${totalItems} cant go more to the right`);  
    }else{
      moveCardsHorizontally(-getNewPosition("right"));  
      activeSlide = activeSlide + 1;
      console.log(`activeSlide + 1 = ${activeSlide}`);
    }  

  }  
};
prevbtn.onclick = function () {
    if(!moving) {
        console.log("prev button clicked");  
        // temporarily disable interactivity
        disableInteraction();
        if(activeSlide === 0){
          console.log("active slide is 0 cant go more to the left");  
        }else{
          moveCardsHorizontally(-getNewPosition("left"));
          activeSlide = activeSlide-1;
          console.log(`activeSlide - 1 = ${activeSlide}`);
        }  
    }  
};
Enter fullscreen mode Exit fullscreen mode

So now you should have your cards move left and right when clicking on the arrow icons like so.

Image description

Then we need to remove the active class from the current card and assign it to the next or previous card depending on which arrow was clicked. We will make a new function called toggleActiveClass and send two parameters one for the oldIndex and another for the newIndex. Since we will already know what the active slide is and what the news or previous slide will be we can just send them as parameters here and remove and add the active class. We also need to update the list of dots by removing the active class and assigning it to the next or previous element. So we need to make a new constant called dots with the elements with .dot class on them.

const dots = document.querySelectorAll('.dot');

  function toggleActiveClass(oldIndex, newIndex){
    //remove all active class    
    items[oldIndex].classList.remove('active');
    dots[oldIndex].classList.remove('active');      
    //add active class to the one passed in by parameter
    items[newIndex].classList.add('active');  
    dots[newIndex].classList.add('active');        
  }
Enter fullscreen mode Exit fullscreen mode

Lets add this function in the event listeners for the next and previous onclick events. We will need to make a separate variable to store the old index and then add it just below the line where we increment or decrease the index and send both variables as parameters.

let storeOldIndex = "";
nextbtn.onclick = function () {
    // Check if carousel is moving, if not, allow interaction
  if(!moving) {
    console.log("next button clicked");  
    // temporarily disable interactivity
    disableInteraction();    
    //total 5 but array starts at 0 so 4
    if(activeSlide === totalItems-1){
      console.log(`active slide is activeSlide:${activeSlide} = totalItems:${totalItems} can't go more to the right`);  
    }else{
      storeOldIndex = activeSlide;
      moveCardsHorizontally(-getNewPosition("right"));  
      activeSlide = activeSlide + 1;
      console.log(`activeSlide + 1 = ${activeSlide}`);
      toggleActiveClass(storeOldIndex, activeSlide);
    }      
  }  
};
prevbtn.onclick = function () {
    if(!moving) {
        console.log("prev button clicked");  
        // temporarily disable interactivity
        disableInteraction();
        if(activeSlide === 0){
          console.log("active slide is 0 cant go more to the left");  
        }else{
          moveCardsHorizontally(-getNewPosition("left"));
          storeOldIndex = activeSlide;
          activeSlide = activeSlide-1;
          console.log(`activeSlide - 1 = ${activeSlide}`);
          toggleActiveClass(storeOldIndex, activeSlide);
        }  
    }  
};
function toggleActiveClass(oldIndex, newIndex){
    //remove all active class    
    items[oldIndex].classList.remove('active');
    dots[oldIndex].classList.remove('active');      
    //add active class to the one passed in by parameter
    items[newIndex].classList.add('active');  
    dots[newIndex].classList.add('active');        
  }
Enter fullscreen mode Exit fullscreen mode

So now when we click on the arrow icons the active class switches from one to the next on both the cards and the dots.

Image description

Ok so that takes care of the arrow navigation actions, now we need to add an event listener to all of the dots so depending on which one is clicked we call the moveNext or MovePrev functions n amount of times.

So we need to determine depending on the dot clicked if we need to call move left or move right. We will do this by comparing if the index obtained from the eventlistener is greater than or less than the activeSlide variable.

I made a separate function to take care of the logic for moving the cards left or right since I saw this was getting repeated all over.

/*DON'T REPEAT YOURSELF*/            
              moveCardsHorizontally(-getNewPosition("left"));          
              storeOldIndex = activeSlide;
              console.log(`storeOldIndex= ${storeOldIndex}`);
              activeSlide = activeSlide-1;
              console.log(`activeSlide - 1 = ${activeSlide}`);
              toggleActiveClass(storeOldIndex, activeSlide);
Enter fullscreen mode Exit fullscreen mode

So I made it into its own function and replaced it in all 3 event listeners.

document.querySelectorAll('.dot').forEach((item, index) => {
  item.addEventListener('click', e => {      
    console.log('clicked on dot');
    //check what index was selected
    console.log(`index:${index} and item:${item}`);          
    //get current slide and determine if the difference is greater or less than
    if(index >  activeSlide){
      console.log(`index:${index} > activeSlide:${activeSlide} move right`);
      moveCardsDotsHorizontally("right");
    }else{
      console.log(`index:${index} < activeSlide:${activeSlide} move left`);
      moveCardsDotsHorizontally("left");
    }
  })
});
  function moveCardsDotsHorizontally(dir){
    moveCardsHorizontally(-getNewPosition(dir));
    storeOldIndex = activeSlide;
    console.log(`storeOldIndex= ${storeOldIndex}`);
    if(dir === "left"){    
      activeSlide = activeSlide-1;
      console.log(`activeSlide - 1 = ${activeSlide}`);
    }else{
      activeSlide = activeSlide+1;
      console.log(`activeSlide + 1 = ${activeSlide}`);
    }
    toggleActiveClass(storeOldIndex, activeSlide);  
  }
nextbtn.onclick = function () {
    // Check if carousel is moving, if not, allow interaction
  if(!moving) {
    console.log("next button clicked");  
    // temporarily disable interactivity
    disableInteraction();    
    //total 5 but array starts at 0 so 4
    if(activeSlide === totalItems-1){
      console.log(`active slide is activeSlide:${activeSlide} = totalItems:${totalItems} cant go more to the right`);  
    }else{      
      moveCardsDotsHorizontally("right");
    }      
  }  
};
prevbtn.onclick = function () {  
    if(!moving) {
        console.log("prev button clicked");  
        // temporarily disable interactivity
        disableInteraction();
        if(activeSlide === 0){
          console.log("active slide is 1 cant go more to the left");  
        }else{
          moveCardsDotsHorizontally("left");            
        }  
    }  
};
Enter fullscreen mode Exit fullscreen mode

This lets us navigate using the dots from left to right like in the arrows, but it only lets us move one at a time, if we try to select the last dot from the first dot it only moves once since we are only calling the function once. We need to determine the difference and call this function the amount of times to the right or left so that these dots help users move faster through each card.

let differenceToMove = "";
let amountOfTimesToLoopLeft = 0;
let amountOfTimesToLoopRight = 0;


function determineDifference(initial, final) {  
  differenceToMove = Math.floor(final - initial);
  console.log(`differenceToMove = final:${final} - initial:${initial} = ${differenceToMove}`);
  return differenceToMove;
}
document.querySelectorAll(".dot").forEach((item, index) => {
  item.addEventListener("click", (e) => {
    console.log("clicked on dot");
    //check what index was selected
    console.log(`index:${index} and item:${item}`);
    //get current slide and determine if the difference is greater or less than
    if (index > activeSlide) {
      console.log(`index:${index} > activeSlide:${activeSlide} move right`);      
      amountOfTimesToLoopRight = Math.floor(determineDifference(index, activeSlide) * -1);
      console.log(`amountOfTimesToLoopRight=${amountOfTimesToLoopRight}`);
      for(var i=0;i<amountOfTimesToLoopRight;i++){
        moveCardsDotsHorizontally("right");
      }
    } else {
      console.log(`index:${index} < activeSlide:${activeSlide} move left`);
      amountOfTimesToLoopLeft = determineDifference(index, activeSlide);  
      console.log(`amountOfTimesToLoopLeft=${amountOfTimesToLoopLeft}`);    
      for(var j=0;j<amountOfTimesToLoopLeft;j++){
        moveCardsDotsHorizontally("left");
      }      
    }    
  });
});
Enter fullscreen mode Exit fullscreen mode

Now that we have gotten the direction let's get the amount of times we need to call the next or prev function.

**

Our result

**
https://codepen.io/Min11Benja/pen/MWzMMXO?editors=0010

We have a slick carousel with no jQuery or external libraries, with single and multiple navigation options.

**

Challanges faced

**

So initially I was calculating the total amount of pixels to move the cards right or left by getting the parent elements width and dividing it up by the amount of slides

singleAmountToMoveHorizontally = Math.floor(totalWidth / totalItems); //292
Enter fullscreen mode Exit fullscreen mode

But this kept giving me poor results in terms of the card showing too much to the left or right.

Image description

So then I started poking around the items object to see if I could find the real width, and I found something called the offsetWidth

I replaced the previous instruction with this one and I got a more reliable result in terms of the amount of pixels that the carousel moved the cards left or right, on different viewports.

singleAmountToMoveHorizontally = items[0].offsetWidth;  //422
Enter fullscreen mode Exit fullscreen mode

Image description

**

What I would do different

**

Now let's refactor this code, remove all of the console logs, add a couple of safeguard rules so as to not get undefined, and add TypeScript to all the variables.

To avoid conflicts and to make this as portable as possible, it’ll be in our best interest to shield our code from the global scope, so we’ll wrap it up in an IIFE.

!(function(d){
  // All code will go in here. We've renamed 'document' to 'd'.
}(document));
Enter fullscreen mode Exit fullscreen mode

I'm not sure if I should redo this using functional programming or not. Other than removing the console logs and adding some if statements to check for undefined variables I could not think of how else I could make this any better, so let's see what CHAT GPT can do to help us improve this code.

I copied and pasted all of the functions on this code and asked it to help me refactor them. This is what it suggested.

CHAT GPT - “I added TypeScript annotations to improve readability and maintainability. I've also made some structural changes to make the code more organised. Please note that I added type annotations to variables and functions, and cleaned up the code where needed.“

I think CHAT GPT did a great job at reducing the code size and removing extra stuff. Here is a link to that version in CodePen.

https://codepen.io/Min11Benja/pen/BaGXQdq?editors=0010
I'm curious how you would have done this carousel any differently? Let me know what you think in the comments below please.

Top comments (0)