How can I implement a solution that allows users to navigate through slides using the mouse on desktop devices and using touch gestures (finger swipe) on mobile devices, without relying on third-party libraries? I need the navigation to be intuitive and smooth, with support for both mouse drag and touch events. The goal is to create a responsive slider that works seamlessly across different screen sizes and input methods
const paginationCirclesContainerEl = document.querySelector('.pagination_circles_container');
const btnPrevPositionEl = document.querySelector('.btn_prev_position');
const btnNextPositionEl = document.querySelector('.btn_next_position');
const sliderRobotsTrackEl = document.querySelector('.slider_robots_track');
const sliderRobotWrapperEls = [...document.querySelectorAll('.slider_robot_wrapper')];
// Variables for working with slides
const visibleSlidesCount = 3;
let currentSlideIndex = visibleSlidesCount;
let isAnimating = false;
// Creating pagination circles equal to the number of slides
const paginationCircles = [];
const createPaginationCircles = () => {
const li = document.createElement('li');
li.className = 'pagination_circle';
paginationCirclesContainerEl.appendChild(li);
paginationCircles.push(li);
};
const addPagination = () => {
sliderRobotWrapperEls.forEach(createPaginationCircles);
paginationCircles[0].classList.add('circle_active');
};
addPagination();
// Base slide width
let slideWidth = sliderRobotWrapperEls[0].offsetWidth;
// Cloning the first three slides
const lastThreeSlides = sliderRobotWrapperEls.slice(0, visibleSlidesCount);
lastThreeSlides.forEach(slideEl => {
const clone = slideEl.cloneNode(true);
sliderRobotsTrackEl.append(clone);
});
// Cloning the last three slides
const firstThreeSlides = sliderRobotWrapperEls.slice(-visibleSlidesCount);
firstThreeSlides.forEach(slideEl => {
const clone = slideEl.cloneNode(true);
sliderRobotsTrackEl.insertBefore(clone, sliderRobotWrapperEls[0]);
});
// New list of slides (12)
const allSlides = [...document.querySelectorAll('.slider_robot_wrapper')];
let isDragging = false;
let startX = 0;
let currentTranslate = 0;
let prevTranslate = 0;
sliderRobotsTrackEl.addEventListener('mousedown', e => {
isDragging = true;
startX = e.clientX;
});
sliderRobotsTrackEl.addEventListener('mousemove', e => {
if (!isDragging) return;
const currentX = e.clientX;
const diffX = currentX - startX;
currentTranslate = prevTranslate + diffX;
updateSliderPosition();
});
sliderRobotsTrackEl.addEventListener('mouseup', e => {
if (!isDragging) return;
isDragging = false;
prevTranslate = currentTranslate;
});
// Dynamically update the slide width on resize
const updateSlideWidth = () => {
slideWidth = allSlides[0].offsetWidth;
updateSliderPosition();
};
window.addEventListener('resize', updateSlideWidth);
// Move the track with slides
const updateSliderPosition = (withTransition = true, index) => {
if (index) {
removeActiveClass(currentSlideIndex);
currentSlideIndex = index;
addActiveClass(currentSlideIndex);
}
const offset = -currentSlideIndex * slideWidth;
sliderRobotsTrackEl.style.transition = withTransition ? 'transform .5s' : 'none';
sliderRobotsTrackEl.style.transform = `translate3d(${offset}px, 0px, 0px)`;
};
// Navigation through pagination circles
paginationCircles.forEach((circleEl, index) => {
circleEl.addEventListener('click', () => {
updateSliderPosition(true, index + visibleSlidesCount);
});
});
// Add active circle class to the current slide
const addActiveClass = (currentSlideIndexCircle) => {
let normalizedIndex;
if (currentSlideIndexCircle) {
normalizedIndex = (currentSlideIndexCircle - visibleSlidesCount + paginationCircles.length) % paginationCircles.length;
} else {
normalizedIndex = (currentSlideIndex - visibleSlidesCount + paginationCircles.length) % paginationCircles.length;
}
paginationCircles[normalizedIndex].classList.add('circle_active');
};
// Remove active circle class from the previous slide
const removeActiveClass = (currentSlideIndexCircle) => {
let normalizedIndex;
if (currentSlideIndexCircle) {
normalizedIndex = (currentSlideIndexCircle - visibleSlidesCount + paginationCircles.length) % paginationCircles.length;
} else {
normalizedIndex = (currentSlideIndex - visibleSlidesCount + paginationCircles.length) % paginationCircles.length;
}
paginationCircles[normalizedIndex].classList.remove('circle_active');
};
// Show the next slide
const nextSlide = () => {
// Block click until animation is finished
if (isAnimating) return;
isAnimating = true;
setTimeout(() => {
isAnimating = false;
}, 500);
removeActiveClass();
currentSlideIndex++;
updateSliderPosition();
addActiveClass();
// Quick rewind when reaching the last clone
if (currentSlideIndex === allSlides.length - visibleSlidesCount) {
setTimeout(() => {
currentSlideIndex = visibleSlidesCount;
updateSliderPosition(false);
}, 500);
}
};
// Show the previous slide
const prevSlide = () => {
// Block click until animation is finished
if (isAnimating) return;
isAnimating = true;
setTimeout(() => {
isAnimating = false;
}, 500);
removeActiveClass();
currentSlideIndex--;
updateSliderPosition();
addActiveClass();
// Quick rewind when reaching the first clone
if (currentSlideIndex === 0) {
setTimeout(() => {
currentSlideIndex = allSlides.length - visibleSlidesCount * 2;
updateSliderPosition(false);
}, 500);
}
};
// Event handlers for the buttons
btnNextPositionEl.addEventListener('click', nextSlide);
btnPrevPositionEl.addEventListener('click', prevSlide);
// Initialize the initial position of the slider
updateSliderPosition(false);
ol {
list-style: none;
}
.slider_robots {
display: flex;
flex-direction: column;
align-items: center;
padding: 80px 0 0 20px;
margin: 0 20px 80px 0;
}
.slider_robots_container {
position: relative;
width: clamp(414px, 100%, 1282px);
margin-bottom: 45px;
overflow: hidden;
}
.slider_btn_container {
position: absolute;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
background-color: #f7f7f7;
border-radius: 100%;
width: 54px;
height: 54px;
cursor: pointer;
}
.btn_prev_position {
left: 0;
top: 380px;
}
.btn_next_position {
right: 0;
top: 380px;
}
.slider_btn {
width: 11px;
height: 11px;
border-top: 2px solid #6a768c;
border-right: 2px solid #6a768c;
}
.btn_prev {
transform: rotate(-135deg);
margin-left: 3px;
}
.btn_next {
transform: rotate(45deg);
margin-right: 3px;
}
.slider_robots_track {
display: flex;
padding: 0 15px;
transition: transform .5s;
}
.slider_robot_wrapper {
display: flex;
flex-direction: column;
flex-grow: 0;
flex-shrink: 0;
flex-basis: calc((100% - 2 * 20px) / 3);
gap: 25px;
padding-right: 20px;
pointer-events: none;
}
.slide_robot {
width: 414px;
height: 414px;
border-radius: 12px;
pointer-events: auto;
}
.pagination_circles_container {
display: flex;
justify-content: center;
width: 100%;
gap: 24px;
}
.pagination_circle {
background-color: #b8edb7;
border-radius: 100%;
width: 10px;
height: 10px;
cursor: pointer;
}
.circle_active {
background-color: #b8edb7;
width: 10px;
height: 10px;
border-radius: 100%;
box-shadow: 0 0 0 7px #447355;
}
@media screen and (max-width: 1024px) {
.slider_robots_container {
width: clamp(462px, 100%, 942px);
}
.slider_robot_wrapper {
flex-basis: calc((100% - 20px) / 2);
}
}
@media screen and (max-width: 768px) {
.slider_robots_container {
width: clamp(100px, 100%, 687px);
}
.slider_robot_wrapper {
flex-basis: 100%;
}
}
<div class="slider_robots">
<div class="slider_robots_container">
<div class="slider_btn_container btn_prev_position">
<button class="slider_btn btn_prev"></button>
</div>
<div class="slider_robots_track">
<div class="slider_robot_wrapper">
<div class="slide_robot" style="background-color: #A8DADC;"></div>
</div>
<div class="slider_robot_wrapper">
<div class="slide_robot" style="background-color: #457B9D;"></div>
</div>
<div class="slider_robot_wrapper">
<div class="slide_robot" style="background-color: #F4A261;"></div>
</div>
<div class="slider_robot_wrapper">
<div class="slide_robot" style="background-color: #2A9D8F;"></div>
</div>
<div class="slider_robot_wrapper">
<div class="slide_robot" style="background-color: #E76F51;"></div>
</div>
<div class="slider_robot_wrapper">
<div class="slide_robot" style="background-color: #264653;"></div>
</div>
</div>
<div class="slider_btn_container btn_next_position">
<button class="slider_btn btn_next"></button>
</div>
</div>
<ol class="pagination_circles_container"></ol>
</div>
Hi you can try this three ways to make slider that allow scrolling (navigate) by touch, mouse or mouse wheel in desktop and mobile
first way :
const slider = document.getElementById("slider");
var slidertouch=false
var lastpos=0;
var immpos=0;
var moveby=5;//speed of touch
var wheelmoveby = 50;//speed of wheel
slider.onmousedown=()=>{
slidertouch=true
}
slider.onmouseup=()=>{
slidertouch=false
}
slider.onblur=()=>{
slidertouch=false
}
slider.onmouseleave =()=>{
slidertouch=false
}
slider.onmousemove=(e)=>{
if(slidertouch){
lastpos=immpos;
immpos=e.x;
if((lastpos-immpos)<0){ // change < to > (reversed)
slider.scrollBy(-moveby, 0);
}else{
slider.scrollBy(moveby, 0);
}
console.log("touch",lastpos,immpos,"/",e.x)
}
}
slider.onmousewheel=(e)=>{
if(e.wheelDelta>0){ // change < to > (reversed)
slider.scrollBy(-wheelmoveby, 0);
}else{
slider.scrollBy(wheelmoveby, 0);
}
console.log("wheel",e.wheelDelta,"/",e.x)
}
const dragcon = document.getElementById("dragcon");
let drag=null;
slider.ondrop = (e) => {
e.preventDefault();
slider.appendChild(drag)
}
slider.ondragover = (e) => {
e.preventDefault();
}
dragcon.ondrop = (e) => {
e.preventDefault();
dragcon.style.backgroundColor = "#00fa00";
dragcon.appendChild(drag)
}
dragcon.ondragover = (e) => {
e.preventDefault();
}
for (let slide of slider.children) {
slide.ondragstart = (e) => {
e.target.style.color = "#00fa00";
//drag=e.target.cloneNode(true)//to get copy of the slide
drag = e.target;
// e.dataTransfer.setData("slide", e.target.id);
}
slide.ondrag = () => {
}
slide.onmousedown = (e) => {
setTimeout(() => { e.target.setAttribute("draggable", "true"); }, 100);
}
slide.onmouseup = (e) => {
e.target.setAttribute("draggable", "false"); e.target.style.color = "#ffffff";
}
slide.onblur = (e) => {
e.target.setAttribute("draggable", "false"); e.target.style.color = "#ffffff";
}
slide.onmouseleave = (e) => {
e.target.setAttribute("draggable", "false"); e.target.style.color = "#ffffff";
}
}
.slides {
scrollbar-color: transparent transparent;/*hide scroll*/
overflow-y: hidden;
overflow-x: scroll;
width: 400px;/*try other width ex: 530 */
padding: 30px 0px;
background: #f2f2f2;
display: flex;
/* scroll-snap-type: x mandatory;*/
/* scroll-snap-type: x proximity;*/
}
.slides > div {
flex-shrink: 0;
margin: 5px;
height: 100px;
background: black;
color: white;
width: 200px;
scroll-snap-align: center;
text-align: center;
}
.dragcon{
width: 100px;
height: 100px;
background-color: #f0f0f0;
}
.dragcon>div{
width: 100px;
height: 50px;
background-color: #000000;
}
<html>
<head></head>
<body>
<div id="slider" class="slides">
<div>slide number 1</div>
<div>slide number 2</div>
<div>slide number 3</div>
<div>slide number 4</div>
<!--increase the slides-->
</div>
<br>
<div id="dragcon" class="dragcon">Drag here !</div>
</body>
</html>
second way :
const slider = document.getElementById("slider");
var slidertouch = false;
var wheelmoveby =50;//speed of wheel
var sliderscrollXlenght =(slider.children.length)*parseFloat(slider.children[0].getBoundingClientRect().width);
var slidertouchrange = parseFloat(slider.getBoundingClientRect().width);
var mousepos=0,scrollpos=0;
slider.onmousedown=()=>{
slidertouch=true
}
slider.onmouseup=()=>{
slidertouch=false
}
slider.onblur=()=>{
slidertouch=false
}
slider.onmouseleave =()=>{
slidertouch=false
}
slider.onmousemove=(e)=>{
if(slidertouch){
mousepos = (Math.abs(slidertouchrange-e.x)/slidertouchrange)*100;
//scrollpos = Math.abs(((mousepos/100)*sliderscrollXlenght)-sliderscrollXlenght); //get scroll postion equevelant to mouse postion (reversed)
scrollpos = (mousepos/100)*sliderscrollXlenght; //get scroll postion equevelant to mouse postion
slider.scrollTo(scrollpos, 0);
console.log("touch",mousepos,"/",scrollpos,"/")
}
}
slider.onmousewheel=(e)=>{
if(e.wheelDelta>0){ // change < to > (reversed)
slider.scrollBy(-wheelmoveby, 0);
}else{
slider.scrollBy(wheelmoveby, 0);
}
console.log("wheel",e.wheelDelta,"/",e.x)
}
const dragcon = document.getElementById("dragcon");
let drag=null;
slider.ondrop = (e) => {
e.preventDefault();
slider.appendChild(drag)
}
slider.ondragover = (e) => {
e.preventDefault();
}
dragcon.ondrop = (e) => {
e.preventDefault();
dragcon.style.backgroundColor = "#00fa00";
dragcon.appendChild(drag)
}
dragcon.ondragover = (e) => {
e.preventDefault();
}
for (let slide of slider.children) {
slide.ondragstart = (e) => {
e.target.style.color = "#00fa00";
//drag=e.target.cloneNode(true)//to get copy of the slide
drag = e.target;
// e.dataTransfer.setData("slide", e.target.id);
}
slide.ondrag = () => {
}
slide.onmousedown = (e) => {
setTimeout(()=>{e.target.setAttribute("draggable", "true"); }, 100);
}
slide.onmouseup = (e) => {
e.target.setAttribute("draggable", "false"); e.target.style.color = "#ffffff";
}
slide.onblur = (e) => {
e.target.setAttribute("draggable", "false"); e.target.style.color = "#ffffff";
}
slide.onmouseleave = (e) => {
e.target.setAttribute("draggable", "false"); e.target.style.color = "#ffffff";
}
}
.slides {
scrollbar-color: transparent transparent;/*hide scroll*/
overflow-y: hidden;
overflow-x: scroll;
width: 330px; /*try other width ex: 530 */
padding: 30px 0px;
background: #f2f2f2;
display: flex;
/* scroll-snap-type: x mandatory; */
/* scroll-snap-type: x proximity;*/
}
.slides > div {
flex-shrink: 0;
margin: 5px;
height: 100px;
background: black;
color: white;
width: 200px;
scroll-snap-align: center;
text-align: center;
}
.dragcon{
width: 100px;
height: 100px;
background-color: #f0f0f0;
}
.dragcon>div{
width: 100px;
height: 50px;
background-color: #000000;
}
<html>
<head></head>
<body>
<div id="slider" class="slides">
<div>slide number 1</div>
<div>slide number 2</div>
<div>slide number 3</div>
<div>slide number 4</div>
<!--increase the slides-->
</div>
<br>
<div id="dragcon" class="dragcon">Drag here !</div>
</body>
</html>
third way (using mouse wheel only on desktop):
const slider = document.getElementById("slider");
var slidertouch=false
var lastpos=0;
var immpos=0;
var wheelmoveby=50;//speed of wheel
slider.onmousewheel=(e)=>{
if(e.wheelDelta>0){ // change < to > (reversed)
slider.scrollBy(-wheelmoveby, 0);
}else{
slider.scrollBy(wheelmoveby, 0);
}
console.log("wheel",e.wheelDelta,"/",e.x)
}
const dragcon = document.getElementById("dragcon");
let drag=null;
slider.ondrop = (e) => {
e.preventDefault();
slider.appendChild(drag)
}
slider.ondragover = (e) => {
e.preventDefault();
}
dragcon.ondrop = (e) => {
e.preventDefault();
dragcon.style.backgroundColor = "#00fa00";
dragcon.appendChild(drag)
}
dragcon.ondragover = (e) => {
e.preventDefault();
}
for (let slide of slider.children) {
slide.ondragstart = (e) => {
e.target.style.color = "#00fa00";
//drag=e.target.cloneNode(true)//to get copy of the slide
drag = e.target;
// e.dataTransfer.setData("slide", e.target.id);
}
slide.ondrag = () => {
}
slide.onmousedown = (e) => {
e.target.setAttribute("draggable", "true");
}
slide.onmouseup = (e) => {
e.target.setAttribute("draggable", "false"); e.target.style.color = "#ffffff";
}
slide.onblur = (e) => {
e.target.setAttribute("draggable", "false"); e.target.style.color = "#ffffff";
}
slide.onmouseleave = (e) => {
e.target.setAttribute("draggable", "false"); e.target.style.color = "#ffffff";
}
}
.slides {
scrollbar-color: transparent transparent; /*hide scroll*/
overflow-y: hidden;
overflow-x: scroll;
width: 330px;/*try other width ex: 530 */
padding: 30px 0px;
background: #f2f2f2;
display: flex;
/* scroll-snap-type: x mandatory;*/
/* scroll-snap-type: x proximity;*/
}
.slides > div {
flex-shrink: 0;
margin: 5px;
height: 100px;
background: black;
color: white;
width: 200px;
scroll-snap-align: center;
text-align: center;
}
.slides>div::selection {
color: white;
background: transparent;
}
.dragcon{
width: 100px;
height: 100px;
background-color: #f0f0f0;
}
.dragcon>div{
width: 100px;
height: 50px;
background-color: #000000;
}
<html>
<head></head>
<body>
<div id="slider" class="slides">
<div>slide number 1</div>
<div>slide number 2</div>
<div>slide number 3</div>
<div>slide number 4</div>
<!--increase the slides-->
</div>
<br>
<div id="dragcon" class="dragcon">Drag here !</div>
</body>
</html>
when you apply any event to any slide I recommend to use event.stopPropagation()
you can set ::selection
Pseudo-element to slides to remove selection
.slides>div::selection {
color: white;
background: transparent;
}
I hope this help you 😊