How to create a slider using only JavaScript and CSS, without including other libraries
My example below is going to show you how to create the simple slider from images. You can use a solution like this for any group of images you have, you would need a wrapper element for the images and for the navigation elements.
Simplified Markup
When adding the image widget in a sidebar, the output would be more complicated in terms of markup, but for this simple demo, I will simplify the markup completely, let’s say like this, also including the two elements that are going to be used for navigation:
<div class="custom-easy-slider">
<button data-direction="prev">❮</button>
<button data-direction="next">❯</button>
<section>
<b>Slide 1 Title</b>
<a href="#link1" title="Slide 1 Title">
<img width="700" height="200" src="photo1.jpg">
</a>
</section>
<section>
<b>Slide 2 Title</b>
<a href="#link2" title="Slide 2 Title">
<img width="700" height="200" src="photo2.jpg">
</a>
</section>
<section>
<b>Slide 3 Title</b>
<a href="#link3" title="Slide 3 Title">
<img width="700" height="200" src="photo3.jpg">
</a>
</section>
</div>
In this markup, the slide is section element. Whatever is inside the “slide” will need to transition between the two states: active/inactive. By default, I would consider using only one class to specify which slide is active, by adding the active class to that element, and whatever does not have that class would be handled as “inactive” in the custom CSS.
The CSS Code
I would first style the elements so that these are all displayed in the same position, and the images filling in the wrapper (using the aspect-ratio makes it so much easier, as you do not have to focus on the image size itself – you can change the aspect-ratio on different media queries). The slide title will be placed over the image, and the navigation arrows will only display when you move the mouse over the slider (or when you focus the slider) so that these do not cover too much space or attract attention, the main focus would be the image. By default, the first slide would be visible.
I would also put in place a simple CSS:
.custom-easy-slider {
--color: #fff;
--accent: #62569fbb;
aspect-ratio: 16/5;
display: flex;
position: relative;
transition: 0.8s all;
button {
all: unset;
aspect-ratio: 1;
background-color: var(--accent);
color: var(--color);
cursor: pointer;
display: inline-flex;
font-size: 2rem;
height: 3rem;
justify-content: center;
opacity: 0;
position: absolute;
text-align: center;
top: calc(50% - 1.5rem);
transition: 0.2s all;
z-index: 5;
}
button[data-direction="next"] {
right: 0;
}
&:focus-within button,
&:hover button {
opacity: 1;
}
button:hover,
button:focus,
a:hover,
a:focus {
outline: 1px auto highlight;
outline: 1px auto -webkit-focus-ring-color;
outline-offset: -1px;
}
section {
height: 100%;
inset: 0;
opacity: 0;
position: absolute;
transition: 0.8s all;
z-index: 0;
width: 100%;
}
section:first-of-type,
section.active {
opacity: 1;
z-index: 1;
}
section img {
display: flex;
height: 100%;
object-fit: cover;
object-position: center;
width: 100%;
}
section b {
background-color: var(--accent);
bottom: 0;
color: var(--color);
display: inline-block;
font-size: 1rem;
left: 0;
padding: 0.5rem 1.5rem;
position: absolute;
text-transform: uppercase;
}
}
@media all and (max-width: 600px) {
.custom-easy-slider {
aspect-ratio: 16/6;
section b {
display: none;
}
}
}
The JavaScript Code
Additionally, I would need to check if there are any elements that contain the “slides”, by looking for the class .custom-easy-slider selector.
By default, the first slide would be active, and after 5 seconds (in the script this appears as the second parameter for setInterval inside the start function, but you can change it to what you need) the next slide will become active, and so on. However, when I click the navigation arrows, I have to make sure that another transition is not ready to fire, so I have to clear the interval first, so the slider will not glitch.
The JavaScript code is:
window.addEventListener('load', () => {
const customEasySlider = [];
const items = document.querySelectorAll('.custom-easy-slider');
if (!items) {
return;
}
function sliderStart(idx) {
if (!customEasySlider[idx]) {
return;
}
const slider = customEasySlider[idx];
const wrap = slider.item;
const prev = slider.prev;
const next = slider.next;
wrap.dataset.paused = 'false';
prev.setAttribute('tabindex', '-1');
next.setAttribute('tabindex', '-1');
function start() {
slider.timer = setInterval(() => {
nav('next');
}, 5000);
}
function stop() {
clearInterval(slider.timer);
}
function activate(to) {
stop();
slider.slides.forEach((slide, i) => {
const link = slide.querySelector('a');
if (to === i) {
slide.classList.add('active');
link.setAttribute('tabindex', '0');
} else {
slide.classList.remove('active');
link.setAttribute('tabindex', '-1');
}
});
slider.active = to;
start();
}
function nav(direction) {
if (direction === 'next') {
slider.active = slider.active < slider.maxIdx ? slider.active + 1 : 0;
} else {
slider.active = slider.active === 0 ? slider.maxIdx : slider.active - 1;
}
activate(slider.active);
}
function pause() {
slider.paused = true;
wrap.dataset.paused = 'true';
prev.setAttribute('tabindex', '0');
next.setAttribute('tabindex', '0');
prev.focus();
stop();
}
function resume() {
slider.paused = false;
wrap.dataset.paused = 'false';
prev.setAttribute('tabindex', '-1');
next.setAttribute('tabindex', '-1');
start();
}
prev.addEventListener('click', () => {
nav('prev');
});
next.addEventListener('click', () => {
nav('next');
});
activate(0);
document.addEventListener(
'mouseover',
(event) => {
if (event.target === wrap || wrap.contains(event.target)) {
pause();
} else if (slider.paused) {
resume();
}
},
true
);
document.addEventListener(
'focus',
(e) => {
if (e.target === wrap || wrap.contains(e.target)) {
if (!slider.paused) {
pause();
}
} else if (slider.paused) {
resume();
}
},
true
);
document.addEventListener(
'keydown',
(event) => {
if (slider.paused) {
if (event.keyCode === 37 || (event.keyCode === 13 && event.target === prev)) {
event.preventDefault();
nav('prev');
pause();
prev.focus();
return;
}
if (event.keyCode === 39 || (event.keyCode === 13 && event.target === next)) {
event.preventDefault();
nav('next');
pause();
next.focus();
}
}
},
true
);
}
items.forEach((item, idx) => {
const prev = item.querySelector('button[data-direction="prev"]');
const next = item.querySelector('button[data-direction="next"]');
const slides = item.querySelectorAll('section');
if (prev && next && slides.length > 1) {
customEasySlider[idx] = {
idx,
item,
slides,
prev,
next,
active: 0,
maxIdx: slides.length - 1,
timer: 0,
paused: false,
};
sliderStart(idx);
}
});
});
The example allows you to use tab, left and right keys to navigate between the slides, outlines the elements that are in focus (for accessibility), and pauses the transition when you go over the slider or that slider is in focus.
Demo – Easy Slider
When assembling the HTML + CSS + JavaScript, the output would be similar to this.


