A Carousel Without an External Library
Scroll snap is CSS, not JavaScript. With Tailwind CSS and a handful of utility classes you get a touch-capable slider that works natively on every device. Alpine.js adds navigation arrows, dot indicators and keyboard support without loading a single external library.
Table of Contents
- 1. Why Scroll Snap Instead of a Slider Library?
- 2. Basic Structure: Container and Slides with Tailwind CSS
- 3. Understanding Snap Types and Align Options
- 4. Navigation with Alpine.js: Arrows and scrollTo
- 5. Dot Indicators: Detecting the Active Slide with IntersectionObserver
- 6. Multiple Slides Visible at Once: Peek and Partial
- 7. Keyboard Navigation and Accessibility
- 8. Autoplay with Pause on Hover and Focus
- 9. Scroll Snap Slider vs. Libraries Compared
- 10. Summary
- 11. FAQ
1. Why Scroll Snap Instead of a Slider Library?
Slider libraries like Swiper, Splide or Glide.js solve the carousel problem completely, but at a considerable cost: extra dependencies, JavaScript overhead and often hundreds of kilobytes of code, of which a project only ever uses a fraction. In Hyvä Themes projects, jQuery and Knockout.js are deliberately excluded, and external slider libraries likewise run counter to the principle of loading as little JavaScript as possible that is not directly part of the framework. The Tailwind CSS scroll snap slider is the alternative: native in the browser, with no dependencies, with touch support out of the box.
CSS scroll snap has been available in every modern browser since 2019 and today has browser support of over 97 percent. The mechanism is simple: the container defines the snap type, and each slide defines its own snap position. The browser handles the snapping natively, including touch gestures on mobile devices, momentum scrolling on iOS and mouse scrolling on desktop. Tailwind CSS provides all the necessary utilities without requiring any custom CSS classes to be written. Alpine.js only comes into play when navigation arrows, dot indicators or autoplay are needed. Pure CSS sliders without any JavaScript at all are equally possible.
2. Basic Structure: Container and Slides with Tailwind CSS
The basic structure of a Tailwind CSS scroll snap slider consists of two elements: the scroll container and the slide elements inside it. The container gets the classes flex overflow-x-auto snap-x snap-mandatory scroll-smooth. flex places the slides horizontally next to each other, overflow-x-auto allows horizontal scrolling, snap-x sets the snap axis to the X direction, snap-mandatory forces the scroll to always end on a snap point, and scroll-smooth smoothly animates programmatic scrolling.
Each slide gets snap-start flex-shrink-0 w-full. snap-start sets the snap point at the start of the slide, so when scrolling the container always jumps so that the start of the slide lies at the left edge of the container. flex-shrink-0 prevents flexbox from shrinking the slides. w-full gives every slide the full width of the container. That is all the CSS classes needed for a working scroll snap slider, with no JavaScript and no external library. On mobile devices, touch swiping works immediately out of the box.
<!-- Basic Tailwind CSS Scroll Snap Slider, no JavaScript needed -->
<div class="relative overflow-hidden rounded-2xl">
<!-- Scroll container: snap-x mandatory, horizontal flex -->
<div
id="slider"
class="flex overflow-x-auto snap-x snap-mandatory scroll-smooth gap-0"
style="-webkit-overflow-scrolling: touch; scrollbar-width: none;"
>
<!-- Each slide: full width, snap at start -->
<div class="snap-start flex-shrink-0 w-full">
<div class="bg-sky-600 text-white flex items-center justify-center h-64 text-2xl font-bold rounded-2xl">
Slide 1
</div>
</div>
<div class="snap-start flex-shrink-0 w-full">
<div class="bg-indigo-600 text-white flex items-center justify-center h-64 text-2xl font-bold rounded-2xl">
Slide 2
</div>
</div>
<div class="snap-start flex-shrink-0 w-full">
<div class="bg-violet-600 text-white flex items-center justify-center h-64 text-2xl font-bold rounded-2xl">
Slide 3
</div>
</div>
</div>
</div>
3. Understanding Snap Types and Align Options
Tailwind CSS offers several snap classes that control different behaviors. snap-mandatory forces every scroll to land on a snap point, so the scroll never comes to rest between two slides. snap-proximity is softer: snapping only kicks in when the scroll is near a snap point. For a slider, snap-mandatory is almost always the right choice because it prevents two slides from being half visible. For scrollable galleries where the user should be able to stop at any position, snap-proximity feels more natural.
For slide alignment there are snap-start, snap-center and snap-end. snap-start aligns the snap point at the start of the element, so the slide jumps to the left edge of the container. snap-center centers the element in the container, ideal for a peek effect where the previous and next slide each poke out on the sides. snap-end aligns at the end, which is rarely needed. For a classic fullscreen slider, snap-start is the default choice. There is also snap-always, which forces every slide to have its own snap point even when several are visible at once.
4. Navigation with Alpine.js: Arrows and scrollTo
For navigation arrows and programmatic advancing, the Tailwind CSS scroll snap slider needs a minimal amount of Alpine.js. The Alpine.js component keeps track of the current slide index and scrolls the container with scrollTo() to the correct position. The key insight: since all slides have the same width (w-full of the container), the X position of the nth slide is always n × containerWidth. That makes navigation possible without any DOM traversal magic.
Alpine.js allows for very compact code here. The x-data attribute on the container wrapper defines the component with the current index and the scroll functions. The arrow buttons use @click to call prev() and next(). The scroll function calculates the target position and uses scrollTo({ left: target, behavior: 'smooth' }). Since scroll-smooth only applies to CSS-triggered scrolls, behavior: 'smooth' must be given explicitly in the JavaScript here so the Alpine.js navigation is animated too.
<!-- Tailwind CSS Scroll Snap Slider with Alpine.js navigation -->
<div
x-data="{
current: 0,
total: 3,
slider() { return this.$refs.slider },
goTo(index) {
this.current = Math.max(0, Math.min(index, this.total - 1));
// Slide width equals container width (w-full slides)
const width = this.slider().offsetWidth;
this.slider().scrollTo({ left: this.current * width, behavior: 'smooth' });
},
prev() { this.goTo(this.current - 1); },
next() { this.goTo(this.current + 1); }
}"
class="relative overflow-hidden rounded-2xl"
>
<!-- Scroll container -->
<div
x-ref="slider"
class="flex overflow-x-auto snap-x snap-mandatory scroll-smooth"
style="-webkit-overflow-scrolling: touch; scrollbar-width: none;"
>
<div class="snap-start flex-shrink-0 w-full h-72 bg-sky-600 flex items-center justify-center text-white text-2xl font-bold">Slide 1</div>
<div class="snap-start flex-shrink-0 w-full h-72 bg-indigo-600 flex items-center justify-center text-white text-2xl font-bold">Slide 2</div>
<div class="snap-start flex-shrink-0 w-full h-72 bg-violet-600 flex items-center justify-center text-white text-2xl font-bold">Slide 3</div>
</div>
<!-- Previous button, hidden on first slide -->
<button
@click="prev()"
:class="current === 0 ? 'opacity-40 cursor-not-allowed' : 'hover:bg-white/30'"
class="absolute left-3 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-white/20 text-white flex items-center justify-center transition-all"
:disabled="current === 0"
aria-label="Previous slide"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
</button>
<!-- Next button, hidden on last slide -->
<button
@click="next()"
:class="current === total - 1 ? 'opacity-40 cursor-not-allowed' : 'hover:bg-white/30'"
class="absolute right-3 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-white/20 text-white flex items-center justify-center transition-all"
:disabled="current === total - 1"
aria-label="Next slide"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
</button>
</div>
5. Dot Indicators: Detecting the Active Slide with IntersectionObserver
Dot indicators show which slide is currently active and allow jumping directly to a specific slide. The challenge: when the user swipes by touch instead of navigating with an arrow button, the Alpine.js component needs to know which slide has scrolled into view. scrollLeft / containerWidth is an approximation, but it is not reliable with snap momentum. The cleanest solution is the IntersectionObserver: an observer is registered for each slide, reporting when the slide is at least 50 percent visible. When that happens, the current index is updated accordingly.
Alpine.js offers x-init as the right hook to initialize the IntersectionObserver. In the x-init callback, all slide elements are collected with querySelectorAll, an observer is created for each one, and the current value of the Alpine.js data is updated on intersection events. The dots themselves are simple buttons in a flex row. With :class="current === index ? 'bg-white' : 'bg-white/40'" the active dot color switches reactively on every scroll event.
6. Multiple Slides Visible at Once: Peek and Partial
Not every Tailwind CSS scroll snap slider shows exactly one slide at a time. Product carousels, testimonial sliders and image galleries often show one and a half or two slides at once. The "peek" effect, where the next slide pokes out on the side, signals to the user that more content is scrollable. This can be achieved with Tailwind CSS purely through CSS: the container gets padding on the right (pr-8 or pr-16), and the slides get a fixed width smaller than 100 percent (w-4/5 or w-[calc(100%-2rem)]).
For responsive variants, one slide on mobile devices, two on tablets, three on desktop, you combine Tailwind's responsive prefixes. The slides get w-full sm:w-1/2 lg:w-1/3, and the container keeps snap-x snap-mandatory. snap-always on the slides ensures that even with several visible slides, each still has its own snap point. That makes a true step-by-step slider on desktop and a fullscreen slider on mobile devices possible, all with Tailwind utilities and no media-query JavaScript.
7. Keyboard Navigation and Accessibility
A Tailwind CSS scroll snap slider without keyboard navigation is not accessible to keyboard users and screen reader users. The minimum requirements for WCAG conformance: arrow keys navigate between slides, Tab reaches interactive elements within the slides, and the active slide is recognizable to screen readers. Alpine.js makes it possible to listen for keyboard events directly on the container wrapper: @keydown.left.window="prev()" and @keydown.right.window="next()" respond to arrow keys. To avoid conflicts with other elements on the page, the slider should have focus first, so @keydown instead of @keydown.window is the safer variant.
For screen reader support, the slide containers get role="group" and aria-label="Slide N of M". The scroll container itself gets role="region" and aria-label="Image carousel". With aria-live="polite" on a hidden status element you can inform screen readers when the active slide changes. That is more effort than with a fully preconfigured library like Swiper, which has accessibility built in, but it is completely controllable and does not add any framework code.
8. Autoplay with Pause on Hover and Focus
Autoplay sliders are tricky from an accessibility standpoint: WCAG 2.1 requires that automatically moving content be pausable, stoppable or able to be slowed down. A Tailwind CSS scroll snap slider with autoplay must therefore pause on hover over the container and on focus of an interactive element inside the slider. Alpine.js implements this with an interval in x-init and event listeners on the container wrapper.
The autoplay pattern with Alpine.js: x-init starts the interval with setInterval() and stores the ID. @mouseenter and @focusin on the wrapper call clearInterval(). @mouseleave and @focusout restart the interval. An Alpine.js store variable paused can optionally drive a pause button. The interval itself calls next(), which automatically wraps back from the last slide to the first (current = (current + 1) % total). This implementation is complete, requires no external library and respects user interaction.
<!-- Autoplay slider with pause on hover/focus, Alpine.js + Tailwind CSS -->
<div
x-data="{
current: 0,
total: 3,
intervalId: null,
paused: false,
init() {
this.startAutoplay();
},
goTo(index) {
this.current = ((index % this.total) + this.total) % this.total;
const width = this.$refs.slider.offsetWidth;
this.$refs.slider.scrollTo({ left: this.current * width, behavior: 'smooth' });
},
next() { this.goTo(this.current + 1); },
prev() { this.goTo(this.current - 1); },
startAutoplay() {
// Advance to next slide every 4 seconds
this.intervalId = setInterval(() => this.next(), 4000);
},
stopAutoplay() {
clearInterval(this.intervalId);
}
}"
@mouseenter="stopAutoplay()"
@mouseleave="startAutoplay()"
@focusin="stopAutoplay()"
@focusout="startAutoplay()"
class="relative overflow-hidden rounded-2xl"
>
<div x-ref="slider" class="flex overflow-x-auto snap-x snap-mandatory scroll-smooth" style="scrollbar-width: none;">
<div class="snap-start flex-shrink-0 w-full h-72 bg-sky-600 flex items-center justify-center text-white text-2xl font-bold">Slide 1</div>
<div class="snap-start flex-shrink-0 w-full h-72 bg-indigo-600 flex items-center justify-center text-white text-2xl font-bold">Slide 2</div>
<div class="snap-start flex-shrink-0 w-full h-72 bg-violet-600 flex items-center justify-center text-white text-2xl font-bold">Slide 3</div>
</div>
<!-- Dot indicators -->
<div class="absolute bottom-4 left-0 right-0 flex justify-center gap-2">
<template x-for="(_, i) in Array.from({ length: total })" :key="i">
<button
@click="goTo(i)"
:class="current === i ? 'bg-white w-6' : 'bg-white/50 w-2'"
class="h-2 rounded-full transition-all duration-300"
:aria-label="`Slide ${i + 1}`"
></button>
</template>
</div>
</div>
9. Scroll Snap Slider vs. Libraries Compared
The choice between a native Tailwind CSS scroll snap slider and a dedicated library depends on the requirements. The native approach is the better choice whenever touch support, responsive layouts and simple navigation are enough. Libraries make sense when very complex features are needed, such as fade transitions, vertical sliders with complex synchronization, or specific thumbnail navigation.
| Feature | Tailwind CSS Scroll Snap | Swiper / Splide | Recommendation |
|---|---|---|---|
| Bundle size | 0 KB JS (CSS-only) | 30 to 130 KB (min+gz) | Native approach |
| Touch support | Native, free | Yes, including momentum | Equivalent |
| Fade transition | Not native | Built in | Library if needed |
| Accessibility (ARIA) | Manual | Built in | Library for complex requirements |
| Dependencies | None | npm package, updates | Native approach |
For most e-commerce and marketing sites, the native Tailwind CSS scroll snap slider is entirely sufficient. Product image sliders, testimonial carousels, banner sliders and feature showcases usually have no requirements beyond what scroll snap plus Alpine.js can deliver. The decisive advantage: zero external dependencies mean no security updates, no breaking changes from npm package updates and no bundle bloat.
Mironsoft
Hyvä Themes, Tailwind CSS and Alpine.js for Magento 2
Sliders and carousels for your Magento shop?
We implement high-performance slider components with Tailwind CSS scroll snap and Alpine.js. Touch-capable, accessible and without external libraries that bloat your build.
Product Slider
Image carousel, thumbnail navigation and zoom for product detail pages
Banner Slider
Hero banner with autoplay, pause on hover and dot navigation
Testimonials
Carousel for customer reviews with peek effect and keyboard navigation
10. Summary
The Tailwind CSS scroll snap slider is the best choice for carousels and sliders in most web projects: it needs no external dependencies, works natively with touch on all modern devices and can be fully configured with a handful of Tailwind utilities. The container defines the snap type and axis, and the slides define their own snap point. Alpine.js adds navigation, dot indicators, autoplay and keyboard support without adding any external JavaScript libraries. For Hyvä Themes projects, that is the natural choice, because it matches the principles exactly: Alpine.js, Tailwind CSS, no jQuery, no external libraries.
The limits of the native approach show up with very complex requirements such as fade transitions between slides, 3D transform effects, or highly specific accessibility requirements that call for a fully preconfigured ARIA implementation. In those cases, a dedicated library like Splide (under 20 KB) is a reasonable decision. For every other use case: CSS can do it, Tailwind makes it easy, Alpine.js makes it interactive, and no npm package is added.
Tailwind CSS Scroll Snap Slider: the essentials at a glance
Container classes
flex overflow-x-auto snap-x snap-mandatory scroll-smooth, that is all the CSS classes needed for the scroll container.
Slide classes
snap-start flex-shrink-0 w-full for fullscreen slides. For a peek effect: w-4/5 and snap-center.
Alpine.js navigation
scrollTo({ left: index * width, behavior: 'smooth' }), no external library, no jQuery, no Swiper.
Pausing autoplay
@mouseenter="stopAutoplay()" and @focusin="stopAutoplay()", mandatory for WCAG conformance.
11. FAQ: Tailwind CSS Scroll Snap Slider
1What is CSS scroll snap?
2Which Tailwind classes for the slider?
flex overflow-x-auto snap-x snap-mandatory scroll-smooth. Slides: snap-start flex-shrink-0 w-full. No further CSS needed.3snap-mandatory vs. snap-proximity?
4Dot indicators without a library?
x-init. At 50% visibility of the slide, current is updated. Dots reactively bound with :class.5iOS and Android support?
-webkit-overflow-scrolling: touch. Over 97% browser support worldwide.6Implementing a peek effect?
w-4/5, use snap-center. The next slide pokes out on the side, signaling scrollable content.7Is Alpine.js mandatory?
8Pausing autoplay on hover?
@mouseenter="stopAutoplay()" and @focusin="stopAutoplay()" on the wrapper. A WCAG requirement for animated content.9Responsive slide widths?
w-full sm:w-1/2 lg:w-1/3 on slides: one slide on mobile, two on tablet, three on desktop, purely with Tailwind utilities.10Is the slider accessible?
role="region", aria-label on the container, role="group" and aria-label="Slide N of M" on the slides, keyboard events and aria-live for status changes.