Image slider using vanilla HTML, CSS and JavaScript

Eric B
CodeX
Published in
7 min readMay 9, 2024

Lately, I have been working on a small website that does not rely on a framework or an external library. The only complex feature was the display of an image slider.

The requirements for this feature were:

  • Vanilla code (you may use them if you are a beginner looking for experience, or if you want to keep your project with a low footprint and to not depend on exhaustive libraries).
  • Responsive design: it should look nice on both desktops and mobile devices, and also benefit from direct sliding on touch devices.
  • Exact image rendering: the images will not be truncated and their aspect ratio will be preserved.

In this article, I will share three solutions I tried. The first two rely on pure HTML/CSS, but the results lack some aspects. The third solution is the final one that met all my requirements, but it uses a few JavaScript lines.

1. Basic slide toggler using radio buttons and labels

This first solution is basic and relies on HTML and CSS only. It is not a proper slider, as no motion is involved. Still, the implementation is quite easy.

Implementation

The root slider div contains two inner divs: one for the slides and another one for the slider navigation.

<div class="slider">
<div class="slides">
...
</div>
<div class="slider-nav">
...
</div>
</div>

For each slide, the content div is enclosed with a radio input used to store the current selected slide.

<div class="slides">
<div class="slide" >
<input type="radio" id="radio_slider1_slide1" name="slider1" checked="checked">
<div class="content">
<img src="../common/werner-sevenster-JuP0ZG0UNi0-unsplash.jpg">
</div>
</div>
<div class="slide">
<input type="radio" id="radio_slider1_slide2" name="slider1">
<div class="content">
<img src="../common/casey-horner-8ftuBebG3_M-unsplash.jpg" loading="lazy">
</div>
</div>
<div class="slide">
<input type="radio" id="radio_slider1_slide3" name="slider1">
<div class="content">
<img src="../common/laura-smetsers-H-TW2CoNtTk-unsplash.jpg" loading="lazy">
</div>
</div>
</div>

Only one value can be selected within a group of radio elements. Thus, only one slide will be displayed with CSS.

The radio inputs are activated by label elements placed in the slider navigation div:

<div class="slider-nav">
<label for="radio_slider1_slide1"><img src="../common/dot.svg" /></label>
<label for="radio_slider1_slide2"><img src="../common/dot.svg" /></label>
<label for="radio_slider1_slide3"><img src="../common/dot.svg" /></label>
</div>

The radio inputs can also be activated with .previous and .next labels attached to each slide:

<div class="slide">
<input type="radio" id="radio_slider1_slide2" name="slider1">
<div class="slider-arrow previous">
<label for="radio_slider1_slide1">
<img src="../common/arrow-sm-right.svg" />
</label>
</div>
<div class="content">
<img src="../common/casey-horner-8ftuBebG3_M-unsplash.jpg" loading="lazy">
</div>
<div class="slider-arrow next">
<label for="radio_slider1_slide3">
<img src="../common/arrow-sm-right.svg" />
</label>
</div>
</div>

For the style:

The radio buttons are not displayed.

In order to display one slide only, the .content and .slider-arrow divs are not displayed unless they are neighbors with the checked radio button.

.slider .slides .slide input[type=radio],
.slider .slides .slide .slider-arrow,
.slider .slides .slide .content {
display: none;
}

.slider .slides .slide input[type=radio]:checked~.slider-arrow {
display: flex;
}

.slider .slides .slide input[type=radio]:checked~.content {
display: flex;
}

Code

Result

Pros and cons

✅ HTML/CSS only

❌ The dots are just labels, so they are not appropriate for distinguishing the index of the selected slide.

❌ The switch between slides is performed through clicks. It does not benefit from the sliding gesture on touching devices.

2. Scroll slider using anchors and links

In this solution, the radio buttons are removed and for selection the use of labels is replaced by anchor links.

<div class="slider" id="slider1">
<div class="slides">
<div class="slide" id="slider1_slide1">
<div class="content">
<img src="../common/werner-sevenster-JuP0ZG0UNi0-unsplash.jpg">
</div>
</div>
<div class="slide" id="slider1_slide2">
<div class="content">
<img src="../common/casey-horner-8ftuBebG3_M-unsplash.jpg" loading="lazy">
</div>
</div>
<div class="slide" id="slider1_slide3">
<div class="content">
<img src="../common/laura-smetsers-H-TW2CoNtTk-unsplash.jpg" loading="lazy">
</div>
</div>
</div>
<div class="slider-nav">
<a href="#slider1_slide1"><img src="../common/dot.svg" /></a>
<a href="#slider1_slide2"><img src="../common/dot.svg" /></a>
<a href="#slider1_slide3"><img src="../common/dot.svg" /></a>
</div>
</div>

For the CSS part, all slides are displayed inside the .slides container and horizontal scrolling is enabled.

.slider .slides {
overflow-x: scroll;
scrollbar-width: thin;
}

For the .slide width, if you set the width to 100%, then the width of the container will be shared between the child slides. In order for each slide to occupy the 100% width at the minimum, you should better use: “min-width: 100%” on the .slide element.

width vs min-width

For scrolling smoothness and proper alignment of the scroll position (like a magnetic effect), the following properties must be added:

.slides {
scroll-behavior: smooth;
scroll-snap-type: x mandatory;
}

.slide {
scroll-snap-align: center;
}

Code

Pros and cons

✅ HTML/CSS only

✅ With horizontal scrolling, it benefits from the sliding gesture on touching devices.

❌ For the previous and next buttons, the horizontal scrolling behavior prevents from simply binding them to each slide. Otherwise, they would move horizontally with the slide and the result is bad-looking.

❌ For this solution, the dots are just links. Like the first solution, it is not appropriate for distinguishing the position of the selected slide.

❌ With the use of anchors for the slide selection, if you click on these anchors, the vertical scroller on the page is impacted as well. Users don’t expect that behavior for horizontal scrolling. Also, the use of anchors impacts the browsing history. Once again, this behavior is too much for just a slider.

3. Scroll slider using JavaScript

In this solution, the horizontal scrolling is kept. The .slider-nav will now contain radio buttons and previous/next arrows.

<div class="slider" id="slider1">
<div class="slides">
<div class="slide">
<div class="content">
<img src="../common/werner-sevenster-JuP0ZG0UNi0-unsplash.jpg">
</div>
</div>
<div class="slide">
<div class="content">
<img src="../common/casey-horner-8ftuBebG3_M-unsplash.jpg" loading="lazy">
</div>
</div>
<div class="slide">
<div class="content">
<img src="../common/laura-smetsers-H-TW2CoNtTk-unsplash.jpg" loading="lazy">
</div>
</div>
</div>
<div class="slider-nav">
<div class="slider-arrow previous">
<img src="../common/arrow-sm-right.svg" />
</div>
<div class="radios">
<input type="radio" id="radio_slider1_slide1" name="slider1" checked="checked">
<input type="radio" id="radio_slider1_slide2" name="slider1">
<input type="radio" id="radio_slider1_slide3" name="slider1">
</div>
<div class="slider-arrow next">
<img src="../common/arrow-sm-right.svg" />
</div>
</div>
</div>

JavaScript will be used for the following purposes:

  • When a previous or next arrow is clicked, the scrollbar position is updated as well to navigate to the previous or next slide: arrowClicked().
  • When the radio selection (dots) is changed, the scrollbar position is updated accordingly: radioChanged().
  • When the scrollbar position is updated (touch device scrolling), the selection of the radio buttons and the visibility of the previous and next arrows are updated: scrolled().
function arrowClicked(event, direction) {
var slides = event.target.parentElement.parentElement.parentElement.getElementsByClassName('slides')[0];
slides.scrollLeft += direction * slides.scrollWidth / slides.childElementCount;
}

function radioChanged(event) {
var radio = document.getElementById(event.target.id);
var radioIndex = [...radio.parentElement.children].indexOf(radio);
var slides = radio.parentElement.parentElement.parentElement.getElementsByClassName('slides')[0];
slides.scrollLeft = radioIndex / slides.childElementCount * slides.scrollWidth;
}

function scrolled(event) {
var id = event.target.parentElement.id;
var slides = document.getElementById(id).getElementsByClassName('slides')[0];
var scrollRatio = slides.scrollLeft / slides.scrollWidth;

var radioId = 'radio_' + id + '_slide';
var size = slides.childElementCount;

for (let i = 1; i <= size; i++) {
if (scrollRatio + 0.5 / size < i / size) {
document.getElementById(radioId + i).checked = true;

if (i == 1) {
document.getElementById(id).getElementsByClassName('previous')[0].style.visibility = "hidden";
} else {
document.getElementById(id).getElementsByClassName('previous')[0].style.visibility = "visible";
}

if (i == size) {
document.getElementById(id).getElementsByClassName('next')[0].style.visibility = "hidden";
} else {
document.getElementById(id).getElementsByClassName('next')[0].style.visibility = "visible";
}

break;
}
}
}

You may call these functions in the .html file directly. However, I recommand to use event listeners directly in order to improve code seperation and reusability.

document.addEventListener("DOMContentLoaded", function () {

function arrowClicked(event, direction) { ... }

function radioChanged(event) { ... }

function scrolled(event) { ... }

document.querySelectorAll('.slider').forEach(
slider => {
slider.getElementsByClassName('previous')[0].style.visibility = "hidden";

if (slider.childElementCount < 1) {
slider.getElementsByClassName('next')[0].style.visibility = "hidden";
}

slider.querySelectorAll('.slider-arrow.previous img')[0].addEventListener(
'click', event => arrowClicked(event, -1)
);

slider.querySelectorAll('.slider-arrow.next img')[0].addEventListener(
'click', event => arrowClicked(event, 1)
);

slider.addEventListener(
'change', event => { radioChanged(event); }
);

slider.getElementsByClassName('slides')[0].addEventListener(
'scroll', event => scrolled(event)
);
}
);
});

Code

Result

Pros and cons

❌ With the use of JavaScript, we introduce another dependency, thus increase ️maintenability issues.

✅ With horizontal scrolling, it benefits from the sliding gesture on touching devices.

✅ The selection of the slide is preserved (dots and arrow states are updated) with no regard to whether the scroll is initiated by the dots, the arrows or the gesture.

Notice on the image border-radius

On this project, I encountered an issue dealing with:

  • The use of “border-radius”
  • The use of “object-fit: contain” to keep the image aspect-ratio
  • Responsiveness: keeping a static height and letting the width variable

The border-radius may disappear when the image is no longer constrained in height but in width:

I solved this problem by encapsulating the image inside a proxy div .content:

<div class="slide" id="slider1_slide1">
<div class="content">
<img src="../common/werner-sevenster-JuP0ZG0UNi0-unsplash.jpg">
</div>
</div>

with CSS:

.slider .slide {
min-width: 100%;
display: flex;
justify-content: center;
}

.slider .slides .content {
display: flex;
align-items: center;
justify-content: center;
height: 400px;
width: 100%;
}

.slider .slides .content img {
max-width: 100%;
max-height: 400px;
object-fit: contain;
border-radius: 10px;
}

--

--