How to Create an Image Slider using HTML + CSS + JS

A great UI component for a collection of images

Nicholas Epps
9 min readJan 21, 2024
Photo by Luis Alberto on Unsplash

Also known as an Image Carousel, this UI component puts a collection of images that might otherwise eat up whitespace into a single frame. With a uniform location for these images, users can slide from photo to photo, or an auto-sliding script can do it for the user. Without further ado, let’s create an image slider using HTML, CSS, and JS.

The Outline

Before we start writing any code, let’s conceptualize what our image slider looks like and how it behaves. The animation below shows how our image slider will behave when finished.

Outline Animation for the Image Slider

The image inside the blue container is the display image. Everything inside the container will be visible, while everything outside will be hidden. The red container, which contains each image, will move left to display the image to the right and vice versa.

In the blue container, there are navigation buttons for changing the display image. The arrows on the left and right sides change the display image to the next left or right image. If the display image is on either end, the image container will loop back to the opposite end. The dots at the bottom allow the user to navigate to a specific image.

Right off the bat, it’s apparent that we are going to need to use the position property in CSS to layer our HTML elements on top of each other — the visual below shows how our image slider might be perceived in a 3D space.

(I used MS Paint to make this)

The blue container and red container exist in the same plane, but the green navigation container is in front of everything else, which allows the pointer events to be caught by the navigation buttons.

The HTML

Before we start, here’s the Replit page if you need to see the finished code.

First, we’ll start by writing our HTML, which is the shortest part of this tutorial. The HTML is as follows:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image Slider</title>
</head>
<style>
/* Insert styles here */
</style>
<body>
<main>
<div class="slider-container slider-dimensions">
<div class="slider">
<div data-img-url="image_1.jpg" class="slider-dimensions"></div>
<div data-img-url="image_2.jpg" class="slider-dimensions"></div>
<div data-img-url="image_3.jpg" class="slider-dimensions"></div>
</div>
<div class="nav-container slider-dimensions">
<button data-index-change="-1">&lt;</button>
<div class="index-container">
<button></button>
<button></button>
<button></button>
</div>
<button data-index-change="1">&gt;</button>
</div>
</div>
</main>
<script>
/* Insert scripts here */
</script>
</body>
</html>

For the images themselves, we are using <div>tags with the attribute data-img-url="*.png" and we’ll use JavaScript to add the CSSbackground-image property, which works better for a responsive design than using <img> tags.

Inside <div class="index-container"> there are 3 buttons, which are the navigation buttons at the bottom center. If you add or remove images to the slider, make sure you add or remove buttons, so the number of buttons correlates to the number of images.

If you need images to test your image slider, I recommend unsplash.com. They have a sizable collection of freely usable images.

Let’s look at what HTML elements correlate with the containers in the sketch image from earlier.

Before we write the CSS, let’s insert the JavaScript needed to render the images. Put this code in the <script> tag.

const imgs = document.querySelectorAll('[data-img-url]');

imgs.forEach((div) => {
div.style.backgroundImage = `url(./${div.getAttribute('data-img-url')})`;
});

This code loops through each <div> that has the attribute [data-img-url] and adds the background-image property to each<div> with its respective [data-img-url] attribute.

We can’t see the images yet because the <div>s currently have a height of 0, but they’ll become visible as we write our CSS.

The CSS

Let’s add our styling to the slider, class by class. First, we have some base styles.

body {
margin: 0;
}

main {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}

.slider-dimensions {
width: min(100vw, 720px);
aspect-ratio: 3 / 2;
}

The body and main style declarations center the image slider which doesn’t affect the slider itself. The .slider-dimensions class is a utility class that is applied to the slider container, navigation container, and each slider image. It allows for a responsive width that accommodates mobile devices. You can change the second argument in the min function, initially 720px, to increase or decrease the max width of the image slider.

Here’s the CSS selector for .slider-container:

.slider-container {
position: relative;
overflow: hidden;
background-color: lightgray;
}

The position: relative; allows us to keep child elements that have position: absolute; within this element’s borders. overflow: hidden; is especially important, as it allows us to hide the images not being shown.

Now let’s add the CSS for the .slider class and the images themselves, .slider > div:

.slider {
position: absolute;
display: flex;
transform: translateX(-0%);
transition: transform 1s;
}

.slider > div {
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}

The position: absolute; property removes the element from the document flow and moves it to the top of the document or to the closest ancestor element that has a position value that’s not static, which is the .slider-container in this instance.

In the .slider selector, the position: absolute; and display: flex; allow each image <div> to fill the width of the slider while not breaking to a new line. The background-* properties in .slider > div format the background image to fit within whatever dimensions the slider-container has. Below are two examples of what this looks like.

The background image will be resized and centered.

By now you should see your first image in the center of the website. If you don’t, double-check your CSS and HTML, and make sure you added the JS script I included earlier.

Below is the styling for the .nav-container.

.nav-container {
position: absolute;
padding: 0 20px 20px;
box-sizing: border-box;
display: flex;
justify-content: space-between;
align-items: center;
background-image: radial-gradient(transparent, rgba(0,0,0,.3));
opacity: 0;
transition: opacity .2s;
}

.nav-container:hover {
opacity: 1;
}

The .nav-container is only displayed when the mouse is over it due to the .nav-container:hover selector. The other style properties are there to lay out the buttons. Lastly, we’ll style the navigation buttons and add conditional mobile styling with the @media query.

.index-container {
align-self: flex-end;
display: flex;
gap: 30px;
}

.index-container > button {
width: 20px;
height: 20px;
border: none;
border-radius: 20px;
background-color: rgba(255,255,255,.3);
transition: background-color .2s;
cursor: pointer;
}

.index-container > button:hover {
background-color: rgba(255,255,255,.7);
}

button[data-index-change] {
font-size: 40px;
color: white;
background-color: transparent;
border: none;
cursor: pointer;
}

@media (max-width: 500px) {
.nav-container {
padding: 0 0 10px;
}

.index-container {
gap: 20px;
}
}

With all our CSS done, your image-slider should look something like this:

Now let’s make this image slider interactive!

The JavaScript

If you’ve been following, along, your <script> tag should resemble the following:

<script>
const imgs = document.querySelectorAll('[data-img-url]');

imgs.forEach((div) => {
div.style.backgroundImage = `url(./${div.getAttribute('data-img-url')})`;
});
</script>

We’re going to add the rest of the JavaScript after the imgs.forEach function call. First, let’s define the variables that reference the various slider tags.

const slider = document.querySelector('.slider');
const indexButtons = document.querySelectorAll('.index-container > button');
const arrowButtons = document.querySelectorAll('[data-index-change]');
let currIndex = 0;

the currIndex variable will be used to keep track of what image is being currently shown and will be updated as needed.

Next is the slide function, which changes the image slider from displaying the image at currIndex to the parameter, nextIndex.

function slide(nextIndex) {
if (nextIndex < 0) nextIndex = imgs.length - 1;
if (nextIndex >= imgs.length) nextIndex = 0;
indexButtons[currIndex].style.backgroundColor = '';
indexButtons[nextIndex].style.backgroundColor = 'white';
slider.style.transform = `translateX(-${(nextIndex / imgs.length) * 100}%)`;
currIndex = nextIndex;
}

The if statements handle the 2 cases where the left arrow was clicked while displaying the first image, or the right arrow was clicked while displaying the last image. In either case, nextIndex becomes the index for the last image or the first image, respectively. The next two lines change the background color on the index buttons to reflect the image change. The following line uses the transform: translateX(); function to move the .slider element using the calculation, (nextIndex / img.length) * 100 in a negative percentage context. For example, if there were 3 images and nextIndex equals 1, the style declaration will be computed as transform: translateX(-33.3333%);. By using a percentage value and the number of images, we can account for a variable number of images in our image slider.

If you recall the CSS we wrote earlier, we had some transition properties that will come to fruition when we call our slide function. If you want to test it out now, open your HTML file in a browser, go to the developer console, and call slide with an index number as an argument.

Before we move on, we need to add this one line that sets a white background to the first index button since it’s initially the first image shown.

indexButtons[0].style.backgroundColor = 'white';

Now let’s hook up our different navigation buttons to the slide function using event listeners.

indexButtons.forEach((button, index) => {
button.addEventListener('click', () => {
slide(index);
});
});

arrowButtons.forEach((button) => {
button.addEventListener('click', () => {
const indexChange = +button.getAttribute('data-index-change');
slide(currIndex + indexChange);
});
});

The indexButtons.forEach function call adds an event listener to each button, which calls the slide function with its respective index when clicked. The arrowButtons.forEach is similar but has some more logic. The indexChange variable is the value of the data-index-change attribute on the button clicked, which is either -1 or 1, converted to a number using an unary plus. We then call slide with nextIndex equaling currIndex + indexChange.

That’s it! try clicking the different navigation buttons and play around with the image slider. Remember that you’ll need to hover over the slider to make the navigation buttons appear.

Bonus: Auto-sliding feature

Are you interested in souping up your image slider? Do you want to automate the process of clicking to see the next photo? Because honestly, your users don’t have the time or energy to do that. Let’s add some more JavaScript to create an auto-sliding feature.

We’ll want our slider to slide to the next image automatically, but not when the user is hovering over the image slider. Let’s first add the code necessary to start and stop the automatic sliding feature.

const navContainer = document.querySelector('.nav-container');
let id;
let isSliding = false;

function startAutoSlide() {
if (!isSliding) {
id = setInterval(() => slide(currIndex + 1), 5000);
isSliding = true;
}
};

function stopAutoSlide() {
clearInterval(id);
isSliding = false;
};

startAutoSlide();

When startAutoSlide is called, it creates a setInterval which calls slide every 5 seconds. id is the id of the current interval, which is cleared when stopAutoSlide is called. isSliding is a guard boolean to handle the case where startAutoSlide is called twice in a row, which would cause the loss of the id to the former setInterval.

Now let’s add our event listeners to stop the auto slider when the user is hovering over the image slider.

navContainer.addEventListener('mouseover', () => stopAutoSlide());
navContainer.addEventListener('mouseout', () => startAutoSlide());

document.addEventListener('touchstart', (e) => {
if (e.target !== navContainer && !navContainer.contains(e.target))
startAutoSlide();
});

This code requires some explaining. On desktop, the mouseover and mouseout events work as you’d expect, but on mobile, the user has to click on the image slider to trigger the mouseover event.

Mobile simulation in Microsoft DevTools.

However, after testing the image slider on an iPhone, triggering the mouseout event was impossible. So, the touchstart event listener handles the case where the user clicks outside of the image slider on a mobile device. The if statement checks if the user actually clicked outside the image slider.

The finished product should behave like this:

Look Ma, no hands!

Conclusion

With the basic technologies of the web, we have created a robust image slider that works on desktop and mobile devices; it also has an auto-sliding feature, if you chose to add it. Your users will appreciate the interactive and intuitive design of this UI component.

--

--