What is a Tailwind slider?
The name covers two components that have little in common. Carousels put slides in a row and let the visitor move between them, which is how hero banners, testimonials, product rows and image galleries work. Range sliders are input type=”range” elements, where a thumb moves along a track to pick a value, like a price filter or a volume control.
A carousel is mostly a layout and scrolling problem. The range slider is a form control, so the two rarely share any code.
Most visitors know the first type as a carousel interface, a row of slides you move through by swipe or with arrow buttons, and sometimes dots.
Tailwind is a utility-first framework, so it sets widths, gaps, snap alignment and colors but adds no interaction. Arrows and dots need code you write or import, and so does autoplay.
So decide on the type first, since the rest follows from that.
Which Tailwind classes control scroll snap?
The container gets snap-x plus snap-mandatory or snap-proximity, and each slide gets snap-start, snap-center or snap-end.
| Class | Goes on | CSS it sets |
|---|
| snap-x | Container | scroll-snap-type: x var(–tw-scroll-snap-strictness) |
| snap-mandatory, snap-proximity | Container | –tw-scroll-snap-strictness |
| snap-start, snap-center, snap-end | Each slide | scroll-snap-align |
| snap-always | Each slide | scroll-snap-stop: always |
According to Tailwind’s documentation, snapping doesn’t start until the children carry an alignment class. So a snap-x container whose slides have no snap-start does nothing.
With snap-mandatory the container always comes to rest on a snap point. snap-proximity only settles on one when a snap point is close, and the CSS Scroll Snap spec makes proximity the default when no strictness is given.
snap-start suits rows that show several slides at once. For one-slide sliders, where the active slide should sit in the middle, snap-center is the pick.
Flex lays the slides in a row and overflow-x-auto makes that row scrollable. Add shrink-0 so flexbox doesn’t squeeze the slides narrower, basis-* to set how wide each one is, and scroll-smooth to animate programmatic scrolling.
The snap-always class forces a stop at every slide, so a hard flick can’t skip past several. caniuse lists support for scroll-snap-stop: always from Chrome 75, Firefox 103 and Safari 15. MDN cautions that it can also keep visitors from scrolling quickly to the content they want.
For the core properties, MDN marks scroll-snap-type as Baseline widely available, with support across browsers since April 2022.
How to build a Tailwind slider without JavaScript
Classes alone are enough for a working slider.
- Create the track: a flex container with overflow-x-auto, snap-x and snap-mandatory (or snap-proximity).
- Add spacing: gap-4 between slides, plus px-4 and scroll-px-4 so the first slide does not sit flush against the edge.
- Size each slide: shrink-0 keeps it from compressing, snap-start sets the snap point, and a basis class sets its width.
- Hide the scrollbar if the design calls for it, with scrollbar-none in current Tailwind v4 or the arbitrary properties [scrollbar-width:none] and [&::-webkit-scrollbar]:hidden.
- Add scroll-smooth, then test touch swipe, mouse wheel and keyboard scrolling on a real phone.
<div class="flex snap-x snap-mandatory gap-4 overflow-x-auto scroll-smooth px-4 scroll-px-4 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
<div class="shrink-0 basis-[85%] snap-start">Slide 1</div>
<div class="shrink-0 basis-[85%] snap-start">Slide 2</div>
<div class="shrink-0 basis-[85%] snap-start">Slide 3</div>
</div>
The 85% basis leaves a sliver of the next slide on screen. With the scrollbar hidden, that sliver is the only hint that the row scrolls.
Touch, wheel and keyboard scrolling come from the browser, not from your code. Bootstrap’s 6.0 alpha documentation lists CSS scroll snap as the feature behind its own Bootstrap carousel, for exactly that reason.
Setting slides per view at each breakpoint
Slides per view change through prefixed basis classes. Tailwind’s mobile-first approach applies unprefixed classes at every width, so the default is the phone layout and each prefix overrides it upward.
| Viewport | Slides visible | Slide class |
|---|
| Default | 1, with a peek of the next | basis-[85%] |
| md and up | 2 | md:basis-[calc((100%_-_1rem)/2)] |
| lg and up | 3 | lg:basis-[calc((100%_-_2rem)/3)] |
The subtraction accounts for gap-4, which is 1rem on the default spacing scale. Plain basis-1/2 ignores the gap, so two slides plus the gap overflow the container and the second one gets clipped.
How to add arrows, dots and autoplay
Scroll snap includes none of these, so each one is a bit of script. Arrows are a scrollBy() call on the track. Autoplay is a timer that fires that same scroll function. Dots take more work, since the active slide has to be read from an IntersectionObserver.
Arrow buttons
MDN documents scrollBy() on the Element interface with a left offset and a behavior option, and behavior: “smooth” animates the move.
Pass the track’s clientWidth as left for the next button and the negative value for the previous one.
One catch. A full-width step skips slides when several are visible, so step by one slide’s offsetWidth plus the gap instead.
Dot indicators
Observe every slide with the track as the root and a threshold above 0.5, then mark the dot of the intersecting slide as active. Clicking a dot calls scrollIntoView() on the matching slide with inline set to start.
Set block to nearest in that call so the page does not jump vertically. A data-active attribute and a Tailwind data variant style the active dot without extra class juggling.
Autoplay
Autoplay is a timer, not a scroll snap feature.
A setInterval call triggers the same next-slide function the arrow uses. Resetting scrollLeft to 0 at the last slide gives a rewind, not a true loop.
It also needs a pause mechanism. According to the W3C Web Accessibility Initiative, WCAG success criterion 2.2.2 requires one for content that moves automatically, lasts more than five seconds and sits beside other content.
Stop the timer on pointerenter and focusin. Skip it entirely when matchMedia reports prefers-reduced-motion, and add motion-reduce:scroll-auto to turn off scroll-smooth for those visitors.
Native scroll buttons and markers
Chrome 135 added the ::scroll-button() and ::scroll-marker() pseudo-elements, which turn a scroll container into a carousel with arrows and dots written in CSS alone, per Chrome for Developers (March 2025). Each scroll button pages 85% of the scroll area.
Chrome 135 and Edge 135 support them. Firefox and Safari do not.
That matches the support box in the article as of March 2025, and MDN’s compatibility data in October 2026 still shows no support for either pseudo-element in Firefox or Safari. Chromium-only support makes it an enhancement on top of the JavaScript controls, not a replacement for them.
When does a scroll snap slider stop working?
Scroll snap breaks down in a few places.
Infinite loops are the first. A scroll container has two ends, so looping needs script that repositions or duplicates slides. Autoplay isn’t built in either, as covered above.
Tracking the active slide is the second. Outside Chromium’s ::scroll-marker, CSS exposes no current-slide state, so dots and counters need IntersectionObserver code.
Mandatory snapping is the third. MDN warns that content larger than you expect can become impossible to scroll into view, and advises mandatory only where you know how much content sits on screen. Keep mandatory for slides you size yourself, and switch to snap-proximity when slide width is unknown.
Fixed-width card rows and product strips are fine on scroll snap, and so are image galleries that only need swipe.
Reach for a library when the slider must loop, when thumbnails or a counter have to stay in sync, when autoplay needs fine control over timing and pausing, or when slide content changes at runtime.
A pure CSS carousel built from the classes above handles the first group with no script at all.
Once the design lands in the second group, a JavaScript carousel library earns its bundle size. For a plain row of cards, skip it.
Which library or component kit fits a Tailwind slider?
Embla Carousel sits best with a utility-class workflow. Swiper is the one for feature-heavy sliders, and teams that want accessibility handled for them tend to land on Splide. If you’d rather not write markup at all, daisyUI, Flowbite and Preline UI cover that.
What decides it is the gap you’re filling, whether that’s loops, synced controls, effects or fine autoplay timing.
JavaScript libraries compared
| Library | Framework support | Standout |
|---|
| Swiper | React and Vue components, Swiper Element (web component) | Module system: navigation, pagination, autoplay, loop, virtual slides |
| Embla Carousel | React, Vue, Svelte, Solid packages | Headless core extended by plugins such as Autoplay |
| Splide | React and Vue wrappers | Accessibility layer, extensions for Grid, Video and Auto Scroll |
| Keen Slider | Vanilla JS, React, Vue, Angular | Native-feeling touch, free-scroll mode |
| Glide.js | Vanilla JS, modular ESM build | Small core with opt-in Controls and Breakpoints modules |
All five are MIT licensed, so weight and features decide, not licensing.
Sizes below are gzipped, as each project publishes them.
- Keen Slider: about 5.5 KB (Keen Slider README)
- Embla Carousel 8.6.0: 6.9 KB, exactly 6,914 bytes (Bundlephobia)
- Glide.js: about 8 KB on its documentation site, about 7 KB in its README
- Splide: 12 KB (Splide README)
- Swiper: about 20 KB with only navigation and pagination (Swiper’s Embla comparison page)
The sources disagree a bit. Swiper’s Embla comparison page puts Embla’s core at 3 to 5 KB gzipped, while Bundlephobia measures the embla-carousel 8.6.0 package at 6.9 KB, and Glide.js quotes two different weights on its own site and README. Swiper’s Keen Slider comparison page also puts the same navigation-and-pagination setup near 28 KB, not 20 KB.
None of those figures says whether plugins or framework wrappers are counted. Measure your own build before picking a winner over a 2 KB gap.
Stylesheets matter more in a Tailwind project than a few kilobytes. Swiper’s examples import swiper/css and Glide’s setup links glide.core.min.css, so both bring class-based CSS that has to coexist with your utilities. Embla needs only a viewport, a container and the slides themselves, and flex and basis classes cover that layout.
Component kits with ready-made sliders
daisyUI has carousel and carousel-item classes, snap modifiers (carousel-start, carousel-center, carousel-end) and carousel-vertical, and no script is involved. Flowbite uses data-carousel=”slide” to cycle items automatically and “static” to hold them still, with Flowbite’s JavaScript file driving the controls and indicators. Preline UI is the one with the @preline/carousel plugin, which adds slide counters and thumbnails, and its isSnap option pairs with snap-x and snap-mandatory on the container, according to the current v5.0 docs.
A prebuilt Tailwind UI kit saves the markup work. The price is a slider tied to that kit’s class names or data attributes.
On the plus side you get markup and Tailwind classes ready to paste, and Flowbite and Preline UI come with controls and indicators already wired. daisyUI adds no JavaScript at all.
The downsides are that those class names or data attributes lock the slider to one kit, and Flowbite and Preline UI ship their own scripts. Preline UI’s package lists MIT plus a Preline UI Fair Use License, so read the terms before a commercial launch.
If the project already runs daisyUI, its carousel is the same scroll snap idea with names attached. Reach for Flowbite or Preline UI only when you want their controls without writing the script yourself.
How to set up a Tailwind slider in React, Vue and Alpine.js
React and Vue projects usually wrap a library. Alpine.js can drive a scroll snap track directly, and plain HTML just needs a script tag and the same utility classes.
| Framework | Option | Entry point |
|---|
| React | Embla Carousel | embla-carousel-react, useEmblaCarousel hook |
| React | Swiper | swiper/react components or Swiper Element |
| Vue | Embla Carousel | embla-carousel-vue, useEmblaCarousel |
| Alpine.js | Scroll snap track | x-data, x-ref, Intersect plugin |
| Plain HTML | Any library | CDN or npm script plus markup |
In React, install embla-carousel-react, attach the ref from useEmblaCarousel to the outer element, and put the layout in utilities. Embla’s React guide says to keep navigation buttons outside the viewport to avoid drag conflicts.
<div className="overflow-hidden" ref={emblaRef}>
<div className="flex">
<div className="min-w-0 flex-[0_0_100%]">Slide 1</div>
<div className="min-w-0 flex-[0_0_100%]">Slide 2</div>
</div>
</div>
New to the Tailwind side of that stack? Setting up Tailwind CSS inside a React project comes first, and the slider code does not change after that.
Mantine’s Carousel component runs on Embla and expects embla-carousel-react installed beside it (Mantine docs), so a Mantine project already has the engine.
Swiper in React and Vue is a little messier. Swiper’s v14 release notes (June 2026) still list swiper/react and swiper/vue next to swiper/element, with imports unchanged from v12. Ionic’s docs describe the React component as set for removal in a future Swiper release, and Swiper’s Svelte components were already dropped in v9.
For new code, Swiper Element is the safer Swiper entry point.
Alpine.js needs no package for the track itself. Alpine’s Intersect plugin wraps IntersectionObserver, and its .half modifier fires once half of the element is visible.
<div x-data="{ active: 0 }">
<div x-ref="track" class="flex snap-x snap-mandatory overflow-x-auto">
<div x-intersect.half="active = 0" class="w-full shrink-0 snap-start">Slide 1</div>
<div x-intersect.half="active = 1" class="w-full shrink-0 snap-start">Slide 2</div>
</div>
<button @click="$refs.track.scrollBy({ left: $refs.track.clientWidth, behavior: 'smooth' })">Next</button>
</div>
Tailwind generates only the classes it finds in scanned files. When a kit’s JavaScript injects Tailwind classes, register its path with @source, which is how Preline’s setup docs handle it.
How to make a Tailwind slider accessible
The ARIA Authoring Practices carousel pattern is the reference here. The carousel region gets a name, every slide gets a group role and a position label, controls are native buttons, and dot pickers shouldn’t flood the tab order.
<section aria-roledescription="carousel" aria-label="Featured products">
<div tabindex="0" aria-live="polite" class="flex snap-x snap-mandatory overflow-x-auto">
<div role="group" aria-roledescription="slide" aria-label="1 of 4" class="w-full shrink-0 snap-start">...</div>
<div role="group" aria-roledescription="slide" aria-label="2 of 4" class="w-full shrink-0 snap-start">...</div>
</div>
<button type="button" aria-label="Previous slide" class="focus-visible:ring-2">...</button>
<button type="button" aria-label="Next slide" class="focus-visible:ring-2">...</button>
</section>
The tabindex=”0″ on the track gives keyboard users a way to focus it and scroll with the arrow keys.
The carousel takes an aria-label, or aria-labelledby when a visible heading exists. Slides are named by position, such as “1 of 4”, without the word “slide”, because aria-roledescription already says it. The live region should be polite while the carousel is still and off while it rotates. And since aria-roledescription values are written in the page language, translate them.
Native buttons carry keyboard behavior for free, which is why the pattern prefers them to div elements dressed up with ARIA roles.
Dots built as separate buttons add one tab stop per slide. The pattern calls that grouped style the least friendly for keyboard users and offers a tabbed variant that uses the tabs pattern for a single tab stop.
Splide v4 follows the W3C carousel pattern and adds a live region that announces slide changes, per Splide’s accessibility guide. Its i18n option translates the default button text.
On the Swiper side, the A11y module adds roles, labels and a live region, set to polite normally and off when autoplay runs, with slides defaulting to role group. It is opt-in, so import A11y and pass it in modules.
Keyboard handling can misfire on busy pages. Accessible360’s Splide carousel boilerplate turns Splide’s keyboard option off, with a code comment that Splide listens for key events at the document level and moves every carousel on the page when arrow keys are used. Splide v4 already changed that option’s default from true to false, so check which version you ship.
How to style a Tailwind range slider
For a quick recolor, an accent-* class on the native input does it in one step. accent-blue-500, accent-rose-500 or any palette color sets the accent-color property, and Tailwind’s docs show the utility taking hover, focus and breakpoint variants, such as md:accent-pink-500. Which parts of the control take the color varies by browser.
When the design needs its own shape, use appearance-none with arbitrary variants on the thumb and track pseudo-elements.
<input type="range" min="0" max="100" value="40"
class="w-full cursor-pointer appearance-none bg-transparent
[&::-webkit-slider-runnable-track]:h-2 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-slate-200
[&::-webkit-slider-thumb]:-mt-1.5 [&::-webkit-slider-thumb]:h-5 [&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-indigo-600
[&::-moz-range-track]:h-2 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-slate-200
[&::-moz-range-thumb]:h-5 [&::-moz-range-thumb]:w-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:bg-indigo-600" />
Material Tailwind’s React Slider styles its thumb and track through the same arbitrary variants, including a negative top margin on the thumb.
Custom styling tends to break in a few predictable ways.
- Set appearance-none on the input and the thumb first, or the pseudo-element styles do not apply
- Write every rule twice, once per engine, because one unknown pseudo-element in a combined selector invalidates the whole rule
- Offset the WebKit thumb with a negative margin (-mt-1.5 here) when the thumb is taller than the track
- Firefox exposes ::-moz-range-progress for the filled part, while WebKit and Blink have no counterpart, so a filled track there needs a gradient or an extra element driven by a CSS variable
daisyUI skips all of that. Its range class takes color modifiers such as range-primary, sizes from range-xs to range-xl, and variables like –range-thumb and –range-fill for overrides.
A native input has exactly one thumb, so dual-handle sliders need two stacked inputs or a library.
noUiSlider is dependency-free and MIT licensed, with ARIA and keyboard support per its npm README, and start: [20, 80] with connect: true produces a two-handle range. Material Tailwind’s v3 docs include a range slider with two thumbs.
When the design stays simple, a plain CSS range slider on the native input is still the better build, because the browser supplies the keyboard and screen reader behavior.
Tailwind Slider FAQ
How do you lazy load images in a Tailwind slider?
Put loading=”lazy” on every img after the first slide, and leave the first visible one eager so it is not delayed.
For finer control, Alpine’s x-intersect.once modifier can swap in the real image source when a slide scrolls into view.
Which classes turn the same track into a testimonial or hero slider?
Give each slide basis-full and snap-center so only one shows at a time, then change what’s inside.
Testimonials are quote cards with padding and centered text, while a hero slide is a full-width image with object-cover and a heading laid over it.
Is Swiper free to use in a Tailwind project?
Yes. The swiper package is MIT licensed, which covers commercial projects.
Swiper Studio, the visual builder, and the premium templates are separate products sold apart from the library.
Do the slider classes differ between Tailwind v3 and v4?
No. The snap-x, snap-mandatory, snap-start and basis-* utilities read the same in both versions.
Setup is where they differ. Tailwind v4 moves theme configuration into CSS through the @theme directive, and a kit’s JavaScript needs an @source line, as Preline’s docs show.
How do you add a thumbnail gallery to a Tailwind slider?
Place a second row of thumbnail buttons under the main track. Each click calls scrollIntoView() on the matching slide.
The IntersectionObserver that drives the dots can highlight the active thumbnail too. Splide and Preline UI both ship thumbnail sliders ready-made.
Can a slider run on radio inputs and peer-checked alone?
Yes. Hidden radio inputs plus Tailwind’s peer-checked variant switch slides with no script, as Cruip’s CSS-only card slider demonstrates.
The slide count is hard-wired into the markup, and swipe gestures do not work the way they do on a scroll snap track.
Choosing between scroll snap and a library
A Tailwind slider should begin as a plain scroll snap track and move to a library only when the missing features outnumber what a few lines of script can cover.
Build the track with utilities first, patch single gaps with small scripts, and replace the track once looping or synced thumbnails show up.
Switching isn’t free. By the roughly 20 KB figure Swiper publishes against Embla, its navigation-and-pagination build weighs roughly 13 KB more than Embla Carousel 8.6.0, nearly three times as much, and Swiper’s own Keen Slider page cites a heavier setup near 28 KB.
Library markup also differs from a native track, so a late switch means rewriting slide wrappers and controls.
That threshold moves up if Firefox and Safari add the ::scroll-button and ::scroll-marker support that Chrome for Developers listed for Chrome and Edge in March 2025, which MDN’s compatibility data shows they have not yet done as of October 2026.
Slide content comes next, and Tailwind card layouts give each slide a structure worth scrolling to.