This was supposed to be a simple and performant solution to the carousel problem that uses the native scroll-snap-type
and scroll-snap-align
styles to achieve the effect, but I abandoned the attempt after realising that it can't be used for carousels that loop.
Created
February 5, 2024 17:12
-
-
Save nothingnesses/f64555fa1890064cc51ae64fa52cddca to your computer and use it in GitHub Desktop.
Carousel based on scroll snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<script src="https://cdn.jsdelivr.net/npm/@unocss/runtime"></script> | |
</head> | |
<body> | |
<main class="h-screen w-screen bg-white grid items-center justify-items-center"> | |
<div class="carousel-container w-full h-full grid items-center justify-items-center"> | |
<div class="carousel snap-x snap-mandatory [&>*]:snap-start overflow-x-scroll [&::-webkit-scrollbar]:hidden bg-gray h-1/2 max-w-1/2 w-full items-center" style="scrollbar-width: none;"> | |
<div class="h-2/3 grid items-center justify-items-center bg-red">1</div> | |
<div class="h-2/3 grid items-center justify-items-center bg-green">2</div> | |
<div class="h-2/3 grid items-center justify-items-center bg-blue">3</div> | |
<div class="h-2/3 grid items-center justify-items-center bg-red">4</div> | |
<div class="h-2/3 grid items-center justify-items-center bg-green">5</div> | |
<div class="h-2/3 grid items-center justify-items-center bg-blue">6</div> | |
<div class="h-2/3 grid items-center justify-items-center bg-red">7</div> | |
<div class="h-2/3 grid items-center justify-items-center bg-green">8</div> | |
<div class="h-2/3 grid items-center justify-items-center bg-blue">9</div> | |
</div> | |
<div class="carousel-pagination"></div> | |
<div class="carousel-next"></div> | |
<div class="carousel-previous"></div> | |
</div> | |
</main> | |
<script src="script.js"></script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(({ | |
carousel_class = "carousel", | |
container_class = "carousel-container", | |
pagination_class = "carousel-pagination", | |
pagination_bullet_element = "div", | |
pagination_bullet_class = "carousel-bullet", | |
pagination_bullet_class_active = "carousel-bullet-active", | |
previous_class = "carousel-previous", | |
next_class = "carousel-next", | |
momentum_decay = 0.95, | |
momentum_threshold = 0.5, | |
slides_per_view = 1, | |
}) => { | |
const class_to_selector = (cls) => `.${cls}`; | |
document.querySelectorAll(class_to_selector(container_class)).forEach((container) => { | |
const carousel = container.querySelector(class_to_selector(carousel_class)); | |
if (carousel) { | |
// Handle mouse device drag events | |
if ( | |
!( | |
"ontouchstart" in window || | |
navigator.maxTouchPoints > 0 || | |
navigator.msMaxTouchPoints > 0 | |
) | |
) { | |
let momentum_frame_id = 0; | |
const stop_momentum = () => { | |
if (momentum_frame_id) { | |
cancelAnimationFrame(momentum_frame_id); | |
} | |
}; | |
carousel.addEventListener("mousedown", (event) => { | |
const client_x = event.clientX; | |
const scroll_left = carousel.scrollLeft; | |
let movement_x = 0; | |
const scroll_carousel = (event) => { | |
carousel.scrollLeft = scroll_left - (event.clientX - client_x); | |
movement_x = event.movementX; | |
}; | |
const momentum_worker = () => { | |
carousel.scrollLeft -= movement_x; | |
movement_x *= momentum_decay; | |
if (Math.abs(movement_x) > momentum_threshold) { | |
momentum_frame_id = window.requestAnimationFrame(momentum_worker); | |
} else { | |
carousel.classList.add("snap-x"); | |
} | |
}; | |
const stop_scroll = () => { | |
document.removeEventListener("mousemove", scroll_carousel); | |
document.removeEventListener("mouseup", stop_scroll); | |
document.removeEventListener("dragstart", stop_scroll); | |
stop_momentum(); | |
momentum_worker(); | |
}; | |
carousel.classList.remove("snap-x"); | |
stop_momentum(); | |
document.addEventListener("dragstart", stop_scroll); | |
document.addEventListener("mouseup", stop_scroll); | |
document.addEventListener("mousemove", scroll_carousel); | |
}); | |
} | |
// Give a min width to slides based on carousel | |
(() => { | |
carousel.style.display = "flex"; | |
const worker = (carousel_width) => { | |
const slide_width = `${carousel_width / slides_per_view}px`; | |
for (const slide of carousel.children) { | |
slide.style.minWidth = slide_width; | |
} | |
}; | |
worker(carousel.offsetWidth); | |
// Handle resize | |
new ResizeObserver((entries) => | |
entries.forEach((entry) => worker(entry.contentRect.width)) | |
).observe(carousel); | |
})(); | |
// Handle pagination | |
const pagination = container.querySelector(class_to_selector(pagination_class)); | |
if (pagination) { | |
for (let index = 0; index < carousel.children.length; ++index) { | |
const bullet = document.createElement(pagination_bullet_element); | |
bullet.classList.add(pagination_bullet_class); | |
pagination.appendChild(bullet); | |
} | |
if (pagination.children.length > 0) { | |
pagination.children[1].classList.add(pagination_bullet_class_active); | |
} | |
/** @todo */ | |
} | |
// Handle navigation | |
const previous_button = container.querySelector(class_to_selector(previous_class)); | |
if (previous_button) { | |
/** @todo */ | |
} | |
const next_button = container.querySelector(class_to_selector(next_class)); | |
if (next_button) { | |
/** @todo */ | |
} | |
} | |
}); | |
})({}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment