How can you make a JavaScript slider from the scratch? How do slideshows really work? Do we really need JavaScript for carousels nowadays? CSS has come a long way, and it's actually possible to make a pretty decent slider with pure CSS. However, you're still very limited in what you can do with CSS alone, so if you want something more complex, you'll need to build your slider in JavaScript.

But then your next question becomes, how to build one? Well fear not my friend, because by the end of this post you'll know how to build a slider with JavaScript, including navigation buttons and breadcrumbs.

Let's do it!

Create the structure for the slider

First, we'll have to create the basic structure for the slider. The html for our slider will look like this:

<div class="container">
<div class="slider">
<div class="slider__slides">
<div class="slider__slide active">1</div>
<div class="slider__slide">2</div>
<div class="slider__slide">3</div>
<div class="slider__slide">4</div>
</div>
</div>
</div>

So, container is just the element that contains the slider. This can be absolutely any part of your site, but I've just gone for a simple flexbox:

.container {
display: flex;
align-items: center;
justify-content: center;
}

Next is the main slider element. This will hold the slides and the navigation elements:

.slider {
display: block;
position: relative;
width: 100%;
max-width: 600px;
margin: 10px;
background-color: white;
overflow: hidden;
}

You can adjust the size in any way that floats your boat - just make sure there's a position property applied to it.

We also have a container div for the slides, slider__slides and four slider__slide elements, which I've styled like this:


.slider__slides {
width: 100%;
padding-top: 66%;
}

.slider__slide {
display: flex;
align-items: center;
justify-content: center;
font-size: 50px;
font-weight: bold;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: lightblue;
transition: 1s;
opacity: 0;
}

.slider__slide.active {
opacity: 1;
}

.slider__slide:nth-of-type(odd) {
background-color: lemonchiffon;
}

Now, here's where it gets a little interesting. Setting padding-top to a percentage in slider__slides will maintain its aspect ratio.

The the slides themselves are positioned absolutely, top and left are both 0, while width and height are both 100%.

This means that each slide will completely fill the first parent element that has a position property applied to it (that's why I said make sure you do that to the slider element).

This also means that the four slides are stacked directly on top of each other. Notice that I've set the opacity of each slide to 0 - this makes them transparent. But the .slider__slide.active part means that any element which has both the slider__slide and active classes will have an opacity of 1 - making the active slide visible.

Of course in your version, feel free to add whatever content you need to the slides - text, images, or whatever.

Here's what that gives us:

See the Pen on CodePen.

As expected, we can only see slide 1 at the moment. Next we'll create the navigation elements that your user will be interacting with.

Add forwards and backwards buttons to the JavaScript slider

Users look for navigation buttons at the far left and right edges of sliders - so that's where we'll put them!

First let's add these elements to the markup:

<div class="container">
<div class="slider">
<div class="slider__slides">
<div class="slider__slide active">1</div>
<div class="slider__slide">2</div>
<div class="slider__slide">3</div>
<div class="slider__slide">4</div>
</div>
<div id="nav-button--prev" class="slider__nav-button"></div>
<div id="nav-button--next" class="slider__nav-button"></div>
</div>
</div>

As well as the slider__nav-button class, each button has a unique id - we'll need this later so we can target these elements with our JavaScript code.

Again, you can style your buttons however you want - I've just kept it simple with a grey box. Hey, sometimes the simple option is the best:

.slider__nav-button {
position: absolute;
height: 70px;
width: 70px;
background-color: #333;
opacity: .8;
cursor: pointer;
}

I've set opacity to .8, so the buttons will be a little transparent, and won't fully block whatever is behind them. And since these are clickable but not a elements, we need cursor: pointer so the user knows they can click it.

Because these two buttons are at different sides of the slider, we'll need to use slightly different CSS to position each one. We can use the ids for that:

#nav-button--prev {
top: 50%;
left: 0;
transform: translateY(-50%);
}

#nav-button--next {
top: 50%;
right: 0;
transform: translateY(-50%);
}

Setting top to 50% will put the top of the element halfway down the parent. So it won't be perfectly centered vertically - it'll be a little lower than it should be.

The solution is to also add transform: translateY(-50%);. When you translate by a percentage, you move the element by a proportion of its own size. So -50% will perfectly center the button, not matter what its height happens to be. Pretty nice trick!

Adding arrows to the navigation buttons with CSS

At the moment we just have a grey box - we should indicate that these are buttons somehow. There are loads of ways of doing this - you could use an image of an arrow, an SVG, or even just write "Prev" and "Next". But here's another way:

#nav-button--prev::after,
#nav-button--next::after
{
content: "";
position: absolute;
border: solid white;
border-width: 0 4px 4px 0;
display: inline-block;
padding: 3px;
width: 40%;
height: 40%;
}

This uses the after pseudoelement, which means we can add it to the slider without having to add anymore html. This creates a box with a solid white border along only its right and bottom edges.

All we need to do now, is rotate the boxes so that the borders look like arrows, and use 'translate' to make sure they're centred within the button:

#nav-button--next::after{
top: 50%;
right: 50%;
transform: translate(25%, -50%) rotate(-45deg);
}

#nav-button--prev::after {
top: 50%;
right: 50%;
transform: translate(75%, -50%) rotate(135deg);
}

If we put all this togther, here's what we get:

See the Pen on CodePen.

Okay, it's starting to look like a slider! Moving on...

Add breadcrumbs to the slider

Breadcrumbs (or navigation dots or bullets or whatever you want to call them) are actually pretty easy to add. First let's update our HTML once again:

<div class="container">
<div class="slider">
<div class="slider__slides">
<div class="slider__slide active">1</div>
<div class="slider__slide">2</div>
<div class="slider__slide">3</div>
<div class="slider__slide">4</div>
</div>
<div id="nav-button--prev" class="slider__nav-button"></div>
<div id="nav-button--next" class="slider__nav-button"></div>
<div class="slider__nav">
<div class="slider__navlink active"></div>
<div class="slider__navlink"></div>
<div class="slider__navlink"></div>
<div class="slider__navlink"></div>
</div>
</div>
</div>
</div>

Now we have a slider__nav element, which contains four slider__navlink divs - one for each slide. The first one has an active class added to it also, which we can use to make the active breadcrumb stand out from the rest.

Traditionally, breadcrumbs like these go at the bottom of a slider:

.slider__nav {
position: absolute;
bottom: 3%;
left: 50%;
transform: translateX(-50%);
text-align: center;
}

We've put the container for the breadcrumbs 3% away from the bottom of the slider, and used the same centering trick we used with the buttons, only this time horizontally. Now for the breadcrumbs themselves:

.slider__navlink {
display: inline-block;
height: 20px;
width: 20px;
border-radius: 50%;
background-color: #333;
opacity: .8;
margin: 0 10px 0 10px;
}

.slider__navlink.active {
background-color: #fff;
}

Pretty simple stuff here - just using border-radius to make them circles, and setting the background colour to white on the active slide. The rest are filled in with the same grey used in the buttons.

Here's what we get:

See the Pen on CodePen.

We're almost there - we just need to make these buttons actually do something! So it's time to write the JavaScript code.

Putting the JavaScript into this JavaScript slider

First, let's make it easy to deal with our slides and breadcrumbs using JS:

let slides = document.getElementsByClassName("slider__slide");
let navlinks = document.getElementsByClassName("slider__navlink");

The first line goes through our markup, finds every element called slider__slide, and stores a pointer to it in an array called slides. The next line does the same for our breadcrumb elements, this time putting them in an array called navlinks.

We also need to keep track of which slide is currently active, so lets make a variable for that:

let currentSlide = 0;

I've set this to 0 because as you may know, in JavaScript arrays are indexed from 0, not 1. So our first slide is in slides[0], the second is in slides[1], and so on.

Now we need to add event listeners to each of our buttons:

document.getElementById("nav-button--next").addEventListener("click", () => {
changeSlide(currentSlide + 1)
});
document.getElementById("nav-button--prev").addEventListener("click", () => {
changeSlide(currentSlide - 1)
});

When a user clicks one of these buttons, our code will call a function called changeSlide (which we'll create in a minute), and pass an argument - currentSlide + 1 for the forwards button, and currentSlide - 1 for the backwards one. That lets us know which slide the user is trying to move to, so we can put the active class onto that slide, and take it off of the current slide.

As you can see here, I've wrapped the function call within an anonymous function - this is necessary when you want to pass an argument using an event listener. For example, this doesn't work:

document.getElementById("nav-button--next").addEventListener("click", changeSlide(currentSlide + 1));

So that's why you have to put the function call within the () => { } block.

Creating the JavaScript function that will change the slide

Here's what the changeSlide() function looks like:

function changeSlide(moveTo) {
if (moveTo >= slides.length) {moveTo = 0;}
if (moveTo < 0) {moveTo = slides.length - 1;}

slides[currentSlide].classList.toggle("active");
navlinks[currentSlide].classList.toggle("active");
slides[moveTo].classList.toggle("active");
navlinks[moveTo].classList.toggle("active");

currentSlide = moveTo;
}

Let's break this down step-by-step:

  1. changeSlide takes one argument, moveTo, which is the number of the slide you want to change to.
  2. The first line checks if moveTo is equal to or greater than slides.length - that would mean we're on the final slide, and trying to move forward. In this case, we'll set moveTo to 0 - which will take us back to the first slide.
  3. The next line does the same if the user tries to go backwards from the first slide - we'll just loop them to the final slide
  4. The next four lines use the slides and navlinks arrays we created earlier. By using classList.toggle("active"), our code will remove the active class from the current slide, and add it to the one we're trying to move to.
  5. Finally, we set currentSlide to equal moveTo, since that's the active slide now.

As you might remember from earlier, we added transition: 1s to our slider__slides element. This means that the change in opacity that is triggered when we add or remove the active class take 1 second to complete - giving a nice fade-in effect.

But before we look at the end result there's just one more thing we need to do...

Making the breadcrumbs clickable

Picture of Columbo advising you to make your breadcrumbs clickable

Ever tried to click something, that you thought was clickable, but it turned out not to be clickable? Remember that feeling of disappointment that swelled up in your gut? Let's not do that to our users! Let's make it easy for them to jump from one slide to another by clicking the breadcrumbs:

document.querySelectorAll('.slider__navlink').forEach((bullet, bulletIndex) => {
bullet.addEventListener('click', () => {
if (currentSlide !== bulletIndex) {
changeSlide(bulletIndex);
}
})
})

Here we've used querySelectorAll to grab all of the slider__navlink elements, and then we're using forEach to loop through them.

So on the first loop, bullet will point to the first slide, and bulletIndex will equal 0.
On the next loop, bullet will point to the second slide, and bulletIndex will equals 1.
And so on.

The idea is, if they click a button, we know they want to change to slide bulletIndex - so we can just pass that value to our funtion with changeSlide(bulletIndex).

However, we'd better first check if they are already on slide bulletIndex. There's no point fading out and then back in to the same slide. So the if statement ensures we'll only call changeSlide, if currentSlide does not equal bulletIndex.

The final product

OK we're done! Here's what we end up with:

See the Pen on CodePen.

Of course, we've only scractched the surface of what's possible when creating a slider with JavaScript. You have a good framework here that you can experiment with. You can change the shape, style, and position of these elements however you need, and of course, you can play around with different transitions when moving between slides.

But if you want to make something that's not only visually slick but super-performant too, why reinvent the wheel? Take a look at fullPage.js and see if it's the tool for you.

As the name implies fullPage.js helps you create fullscreen scrolling websites. You get a range of different effects that will impress your visitors, it's really easy to set up, and it works with other frameworks and CMSs including React and WordPress.

Take a look!