17 / 22
All 22 in 22 Tailwind CSS Image Carousel
- 01Tailwind E-commerce Product Gallery Carousel with Thumbnails
- 02Tailwind Shop the Look Multi-Item Card Slider with Scroll Snap
- 03Tailwind Infinite Logo Scroller Marquee with No JavaScript
- 04Tailwind Product Colour Variant Picker Slider
- 05Tailwind Full-Screen Hero Slideshow with Auto Cross-Fade
- 06Tailwind Testimonial Card Carousel with Star Ratings
- 07Tailwind Meet the Team Profile Slider with Circular Portraits
- 08Tailwind Portfolio Gallery Slider with Hover Caption Reveal
- 09Tailwind Netflix-Style Content Row Carousel with Hover Expand
- 10Tailwind Magazine Split-Screen Article Slider
- 11Tailwind Onboarding Slider with Native CSS Scroll-Marker Dots
- 12Tailwind Vertical Image Carousel with Y-Axis Scroll Snap
- 13Tailwind 3D Coverflow Carousel with Perspective Transforms
- 14Tailwind Cross-Fade Image Slideshow with Manual Controls
- 15Tailwind Dual Opposite-Direction Marquee Slider
- 16Tailwind Dot Pagination Carousel with Target and Has Selectors
- 17Tailwind Prev / Next Arrow Carousel with Native Scroll Buttons
- 18Tailwind Before / After Image Comparison Slider
- 19Tailwind Accordion Expanding Image Panel Carousel
- 20Tailwind Autoplay Slider with Keyframe Track and Progress Bars
- 21Tailwind Zoom-on-Hover Image Gallery Slider
- 22Tailwind Parallax Text-Over-Image Story Slider
Tailwind Prev / Next Arrow Carousel with Native Scroll Buttons
Real previous/next buttons that page the gallery one slide at a time, generated by the browser via ::scroll-button() — focusable, auto-disabling at the ends, and needing no JavaScript. Anchor-link arrows sit underneath as the universal fallback.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tcar-17 w-full min-h-svh grid place-items-center bg-tcar17-bg font-tcar17-sans px-5 py-10" aria-label="Featured shots">
<div class="relative w-[min(620px,100%)]">
<ul tabindex="0" role="group" aria-label="Shots, use the arrow buttons or arrow keys" class="tcar17-scroller list-none flex overflow-x-auto m-0 p-0 rounded-2xl snap-x snap-mandatory scroll-smooth [scrollbar-width:none] shadow-[0_24px_50px_-34px_oklch(0.35_0.1_265/0.5)] focus-visible:outline-3 focus-visible:outline-tcar17-acc focus-visible:outline-offset-[3px]">
<li id="tcar-17-s1" class="flex-none basis-full snap-center aspect-[16/10] overflow-hidden"><img src="https://picsum.photos/seed/arrow-1/900/560" width="900" height="560" fetchpriority="high" decoding="async" alt="City skyline" class="size-full object-cover block"></li>
<li id="tcar-17-s2" class="flex-none basis-full snap-center aspect-[16/10] overflow-hidden"><img src="https://picsum.photos/seed/arrow-2/900/560" width="900" height="560" loading="lazy" decoding="async" alt="Desert highway" class="size-full object-cover block"></li>
<li id="tcar-17-s3" class="flex-none basis-full snap-center aspect-[16/10] overflow-hidden"><img src="https://picsum.photos/seed/arrow-3/900/560" width="900" height="560" loading="lazy" decoding="async" alt="Rainforest canopy" class="size-full object-cover block"></li>
<li id="tcar-17-s4" class="flex-none basis-full snap-center aspect-[16/10] overflow-hidden"><img src="https://picsum.photos/seed/arrow-4/900/560" width="900" height="560" loading="lazy" decoding="async" alt="Frozen lake" class="size-full object-cover block"></li>
</ul>
<nav aria-label="Slide navigation (fallback)" class="tcar17-fallback absolute inset-0 pointer-events-none">
<a href="#tcar-17-s1" aria-label="Previous" class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-auto size-11 flex items-center justify-center rounded-full bg-white text-tcar17-acc text-[26px] font-bold no-underline shadow-[0_8px_20px_-8px_oklch(0.35_0.1_265/0.6)] transition-[scale] duration-200 hover:scale-[1.08] focus-visible:outline-3 focus-visible:outline-tcar17-acc focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:hover:scale-100">‹</a>
<a href="#tcar-17-s4" aria-label="Next" class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-auto size-11 flex items-center justify-center rounded-full bg-white text-tcar17-acc text-[26px] font-bold no-underline shadow-[0_8px_20px_-8px_oklch(0.35_0.1_265/0.6)] transition-[scale] duration-200 hover:scale-[1.08] focus-visible:outline-3 focus-visible:outline-tcar17-acc focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:hover:scale-100">›</a>
</nav>
</div>
</section><section class="tcar-17 w-full min-h-svh grid place-items-center bg-tcar17-bg font-tcar17-sans px-5 py-10" aria-label="Featured shots">
<div class="relative w-[min(620px,100%)]">
<ul tabindex="0" role="group" aria-label="Shots, use the arrow buttons or arrow keys" class="tcar17-scroller list-none flex overflow-x-auto m-0 p-0 rounded-2xl snap-x snap-mandatory scroll-smooth [scrollbar-width:none] shadow-[0_24px_50px_-34px_oklch(0.35_0.1_265/0.5)] focus-visible:outline-3 focus-visible:outline-tcar17-acc focus-visible:outline-offset-[3px]">
<li id="tcar-17-s1" class="flex-none basis-full snap-center aspect-[16/10] overflow-hidden"><img src="https://picsum.photos/seed/arrow-1/900/560" width="900" height="560" fetchpriority="high" decoding="async" alt="City skyline" class="size-full object-cover block"></li>
<li id="tcar-17-s2" class="flex-none basis-full snap-center aspect-[16/10] overflow-hidden"><img src="https://picsum.photos/seed/arrow-2/900/560" width="900" height="560" loading="lazy" decoding="async" alt="Desert highway" class="size-full object-cover block"></li>
<li id="tcar-17-s3" class="flex-none basis-full snap-center aspect-[16/10] overflow-hidden"><img src="https://picsum.photos/seed/arrow-3/900/560" width="900" height="560" loading="lazy" decoding="async" alt="Rainforest canopy" class="size-full object-cover block"></li>
<li id="tcar-17-s4" class="flex-none basis-full snap-center aspect-[16/10] overflow-hidden"><img src="https://picsum.photos/seed/arrow-4/900/560" width="900" height="560" loading="lazy" decoding="async" alt="Frozen lake" class="size-full object-cover block"></li>
</ul>
<nav aria-label="Slide navigation (fallback)" class="tcar17-fallback absolute inset-0 pointer-events-none">
<a href="#tcar-17-s1" aria-label="Previous" class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-auto size-11 flex items-center justify-center rounded-full bg-white text-tcar17-acc text-[26px] font-bold no-underline shadow-[0_8px_20px_-8px_oklch(0.35_0.1_265/0.6)] transition-[scale] duration-200 hover:scale-[1.08] focus-visible:outline-3 focus-visible:outline-tcar17-acc focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:hover:scale-100">‹</a>
<a href="#tcar-17-s4" aria-label="Next" class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-auto size-11 flex items-center justify-center rounded-full bg-white text-tcar17-acc text-[26px] font-bold no-underline shadow-[0_8px_20px_-8px_oklch(0.35_0.1_265/0.6)] transition-[scale] duration-200 hover:scale-[1.08] focus-visible:outline-3 focus-visible:outline-tcar17-acc focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:hover:scale-100">›</a>
</nav>
</div>
</section>@import "tailwindcss";
@theme {
--color-tcar17-bg: #eef2fb;
--color-tcar17-bg: oklch(0.95 0.015 265);
--color-tcar17-acc: #2563eb;
--color-tcar17-acc: oklch(0.52 0.2 262);
--font-tcar17-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tcar-17 {
width: 100%;
min-height: 100svh;
}
.tcar-17 ::selection {
background: var(--color-tcar17-acc);
color: #fff;
}
.tcar17-scroller::-webkit-scrollbar {
display: none;
}
/* CSS Carousel scroll buttons: no utility form, so the whole control lives here. */
.tcar17-scroller::scroll-button(left),
.tcar17-scroller::scroll-button(right) {
position: absolute;
top: 50%;
translate: 0 -50%;
z-index: 3;
width: 44px;
height: 44px;
border: 0;
border-radius: 50%;
cursor: pointer;
background: #fff;
color: var(--color-tcar17-acc);
font-size: 22px;
font-weight: 700;
line-height: 1;
box-shadow: 0 8px 20px -8px oklch(0.35 0.1 265 / 0.6);
transition: scale .2s, opacity .2s;
}
.tcar17-scroller::scroll-button(left) {
content: "\2039";
left: 12px;
}
.tcar17-scroller::scroll-button(right) {
content: "\203A";
right: 12px;
}
.tcar17-scroller::scroll-button(left):hover,
.tcar17-scroller::scroll-button(right):hover {
scale: 1.08;
}
.tcar17-scroller::scroll-button(left):disabled,
.tcar17-scroller::scroll-button(right):disabled {
opacity: .35;
cursor: default;
}
.tcar17-scroller::scroll-button(left):focus-visible,
.tcar17-scroller::scroll-button(right):focus-visible {
outline: 3px solid var(--color-tcar17-acc);
outline-offset: 2px;
}
/* One rule decides which control set the user sees. */
@supports selector(::scroll-button(right)) {
.tcar17-fallback {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.tcar17-scroller {
scroll-behavior: auto;
}
.tcar17-scroller::scroll-button(left),
.tcar17-scroller::scroll-button(right) {
transition: none;
}
}@import "tailwindcss";
@theme {
--color-tcar17-bg: #eef2fb;
--color-tcar17-bg: oklch(0.95 0.015 265);
--color-tcar17-acc: #2563eb;
--color-tcar17-acc: oklch(0.52 0.2 262);
--font-tcar17-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tcar-17 {
width: 100%;
min-height: 100svh;
}
.tcar-17 ::selection {
background: var(--color-tcar17-acc);
color: #fff;
}
.tcar17-scroller::-webkit-scrollbar {
display: none;
}
/* CSS Carousel scroll buttons: no utility form, so the whole control lives here. */
.tcar17-scroller::scroll-button(left),
.tcar17-scroller::scroll-button(right) {
position: absolute;
top: 50%;
translate: 0 -50%;
z-index: 3;
width: 44px;
height: 44px;
border: 0;
border-radius: 50%;
cursor: pointer;
background: #fff;
color: var(--color-tcar17-acc);
font-size: 22px;
font-weight: 700;
line-height: 1;
box-shadow: 0 8px 20px -8px oklch(0.35 0.1 265 / 0.6);
transition: scale .2s, opacity .2s;
}
.tcar17-scroller::scroll-button(left) {
content: "\2039";
left: 12px;
}
.tcar17-scroller::scroll-button(right) {
content: "\203A";
right: 12px;
}
.tcar17-scroller::scroll-button(left):hover,
.tcar17-scroller::scroll-button(right):hover {
scale: 1.08;
}
.tcar17-scroller::scroll-button(left):disabled,
.tcar17-scroller::scroll-button(right):disabled {
opacity: .35;
cursor: default;
}
.tcar17-scroller::scroll-button(left):focus-visible,
.tcar17-scroller::scroll-button(right):focus-visible {
outline: 3px solid var(--color-tcar17-acc);
outline-offset: 2px;
}
/* One rule decides which control set the user sees. */
@supports selector(::scroll-button(right)) {
.tcar17-fallback {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.tcar17-scroller {
scroll-behavior: auto;
}
.tcar17-scroller::scroll-button(left),
.tcar17-scroller::scroll-button(right) {
transition: none;
}
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tcar17-bg: #eef2fb;
--color-tcar17-bg: oklch(0.95 0.015 265);
--color-tcar17-acc: #2563eb;
--color-tcar17-acc: oklch(0.52 0.2 262);
--font-tcar17-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tcar-17 {
width: 100%;
min-height: 100svh;
}
.tcar-17 ::selection {
background: var(--color-tcar17-acc);
color: #fff;
}
.tcar17-scroller::-webkit-scrollbar {
display: none;
}
/* CSS Carousel scroll buttons: no utility form, so the whole control lives here. */
.tcar17-scroller::scroll-button(left),
.tcar17-scroller::scroll-button(right) {
position: absolute;
top: 50%;
translate: 0 -50%;
z-index: 3;
width: 44px;
height: 44px;
border: 0;
border-radius: 50%;
cursor: pointer;
background: #fff;
color: var(--color-tcar17-acc);
font-size: 22px;
font-weight: 700;
line-height: 1;
box-shadow: 0 8px 20px -8px oklch(0.35 0.1 265 / 0.6);
transition: scale .2s, opacity .2s;
}
.tcar17-scroller::scroll-button(left) {
content: "\2039";
left: 12px;
}
.tcar17-scroller::scroll-button(right) {
content: "\203A";
right: 12px;
}
.tcar17-scroller::scroll-button(left):hover,
.tcar17-scroller::scroll-button(right):hover {
scale: 1.08;
}
.tcar17-scroller::scroll-button(left):disabled,
.tcar17-scroller::scroll-button(right):disabled {
opacity: .35;
cursor: default;
}
.tcar17-scroller::scroll-button(left):focus-visible,
.tcar17-scroller::scroll-button(right):focus-visible {
outline: 3px solid var(--color-tcar17-acc);
outline-offset: 2px;
}
/* One rule decides which control set the user sees. */
@supports selector(::scroll-button(right)) {
.tcar17-fallback {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.tcar17-scroller {
scroll-behavior: auto;
}
.tcar17-scroller::scroll-button(left),
.tcar17-scroller::scroll-button(right) {
transition: none;
}
}@import "tailwindcss";
@theme {
--color-tcar17-bg: #eef2fb;
--color-tcar17-bg: oklch(0.95 0.015 265);
--color-tcar17-acc: #2563eb;
--color-tcar17-acc: oklch(0.52 0.2 262);
--font-tcar17-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tcar-17 {
width: 100%;
min-height: 100svh;
}
.tcar-17 ::selection {
background: var(--color-tcar17-acc);
color: #fff;
}
.tcar17-scroller::-webkit-scrollbar {
display: none;
}
/* CSS Carousel scroll buttons: no utility form, so the whole control lives here. */
.tcar17-scroller::scroll-button(left),
.tcar17-scroller::scroll-button(right) {
position: absolute;
top: 50%;
translate: 0 -50%;
z-index: 3;
width: 44px;
height: 44px;
border: 0;
border-radius: 50%;
cursor: pointer;
background: #fff;
color: var(--color-tcar17-acc);
font-size: 22px;
font-weight: 700;
line-height: 1;
box-shadow: 0 8px 20px -8px oklch(0.35 0.1 265 / 0.6);
transition: scale .2s, opacity .2s;
}
.tcar17-scroller::scroll-button(left) {
content: "\2039";
left: 12px;
}
.tcar17-scroller::scroll-button(right) {
content: "\203A";
right: 12px;
}
.tcar17-scroller::scroll-button(left):hover,
.tcar17-scroller::scroll-button(right):hover {
scale: 1.08;
}
.tcar17-scroller::scroll-button(left):disabled,
.tcar17-scroller::scroll-button(right):disabled {
opacity: .35;
cursor: default;
}
.tcar17-scroller::scroll-button(left):focus-visible,
.tcar17-scroller::scroll-button(right):focus-visible {
outline: 3px solid var(--color-tcar17-acc);
outline-offset: 2px;
}
/* One rule decides which control set the user sees. */
@supports selector(::scroll-button(right)) {
.tcar17-fallback {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.tcar17-scroller {
scroll-behavior: auto;
}
.tcar17-scroller::scroll-button(left),
.tcar17-scroller::scroll-button(right) {
transition: none;
}
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The clearest boundary in the collection: the fallback arrows are 100% utilities, the native buttons are 100% CSS, and
@supportschooses. Nothing is duplicated except the visual language, deliberately. - Both control sets are 44×44px. The original set the native buttons to 44px and the fallback arrows to 44px too, but only by accident of the shadow — here
size-11says it on purpose. - UI change from the CSS original: the fallback arrows are now a real prev/next pair pointing at the first and last slides, and both carry
motion-reduce:branches. The scale-on-hover also moved to thescaleproperty so it composes with the-translate-y-1/2centring instead of overwriting it — the original had to restatetranslateY(-50%)inside every hover rule.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Prev / Next Arrow Carousel with Native Scroll Buttons
Source: https://codefronts.com/snippets/tailwind-css-image-carousel/tailwind-prev-next-arrow-carousel-scroll-button/
Real previous/next buttons that page the gallery one slide at a time, generated by the browser via ::scroll-button() — focusable, auto-disabling at the ends, and needing no JavaScript. Anchor-link arrows sit underneath as the universal fallback.
## HTML
```html
<section class="tcar-17 w-full min-h-svh grid place-items-center bg-tcar17-bg font-tcar17-sans px-5 py-10" aria-label="Featured shots">
<div class="relative w-[min(620px,100%)]">
<ul tabindex="0" role="group" aria-label="Shots, use the arrow buttons or arrow keys" class="tcar17-scroller list-none flex overflow-x-auto m-0 p-0 rounded-2xl snap-x snap-mandatory scroll-smooth [scrollbar-width:none] shadow-[0_24px_50px_-34px_oklch(0.35_0.1_265/0.5)] focus-visible:outline-3 focus-visible:outline-tcar17-acc focus-visible:outline-offset-[3px]">
<li id="tcar-17-s1" class="flex-none basis-full snap-center aspect-[16/10] overflow-hidden"><img src="https://picsum.photos/seed/arrow-1/900/560" width="900" height="560" fetchpriority="high" decoding="async" alt="City skyline" class="size-full object-cover block"></li>
<li id="tcar-17-s2" class="flex-none basis-full snap-center aspect-[16/10] overflow-hidden"><img src="https://picsum.photos/seed/arrow-2/900/560" width="900" height="560" loading="lazy" decoding="async" alt="Desert highway" class="size-full object-cover block"></li>
<li id="tcar-17-s3" class="flex-none basis-full snap-center aspect-[16/10] overflow-hidden"><img src="https://picsum.photos/seed/arrow-3/900/560" width="900" height="560" loading="lazy" decoding="async" alt="Rainforest canopy" class="size-full object-cover block"></li>
<li id="tcar-17-s4" class="flex-none basis-full snap-center aspect-[16/10] overflow-hidden"><img src="https://picsum.photos/seed/arrow-4/900/560" width="900" height="560" loading="lazy" decoding="async" alt="Frozen lake" class="size-full object-cover block"></li>
</ul>
<nav aria-label="Slide navigation (fallback)" class="tcar17-fallback absolute inset-0 pointer-events-none">
<a href="#tcar-17-s1" aria-label="Previous" class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-auto size-11 flex items-center justify-center rounded-full bg-white text-tcar17-acc text-[26px] font-bold no-underline shadow-[0_8px_20px_-8px_oklch(0.35_0.1_265/0.6)] transition-[scale] duration-200 hover:scale-[1.08] focus-visible:outline-3 focus-visible:outline-tcar17-acc focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:hover:scale-100">‹</a>
<a href="#tcar-17-s4" aria-label="Next" class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-auto size-11 flex items-center justify-center rounded-full bg-white text-tcar17-acc text-[26px] font-bold no-underline shadow-[0_8px_20px_-8px_oklch(0.35_0.1_265/0.6)] transition-[scale] duration-200 hover:scale-[1.08] focus-visible:outline-3 focus-visible:outline-tcar17-acc focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:hover:scale-100">›</a>
</nav>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tcar17-bg: #eef2fb;
--color-tcar17-bg: oklch(0.95 0.015 265);
--color-tcar17-acc: #2563eb;
--color-tcar17-acc: oklch(0.52 0.2 262);
--font-tcar17-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tcar-17 {
width: 100%;
min-height: 100svh;
}
.tcar-17 ::selection {
background: var(--color-tcar17-acc);
color: #fff;
}
.tcar17-scroller::-webkit-scrollbar {
display: none;
}
/* CSS Carousel scroll buttons: no utility form, so the whole control lives here. */
.tcar17-scroller::scroll-button(left),
.tcar17-scroller::scroll-button(right) {
position: absolute;
top: 50%;
translate: 0 -50%;
z-index: 3;
width: 44px;
height: 44px;
border: 0;
border-radius: 50%;
cursor: pointer;
background: #fff;
color: var(--color-tcar17-acc);
font-size: 22px;
font-weight: 700;
line-height: 1;
box-shadow: 0 8px 20px -8px oklch(0.35 0.1 265 / 0.6);
transition: scale .2s, opacity .2s;
}
.tcar17-scroller::scroll-button(left) {
content: "\2039";
left: 12px;
}
.tcar17-scroller::scroll-button(right) {
content: "\203A";
right: 12px;
}
.tcar17-scroller::scroll-button(left):hover,
.tcar17-scroller::scroll-button(right):hover {
scale: 1.08;
}
.tcar17-scroller::scroll-button(left):disabled,
.tcar17-scroller::scroll-button(right):disabled {
opacity: .35;
cursor: default;
}
.tcar17-scroller::scroll-button(left):focus-visible,
.tcar17-scroller::scroll-button(right):focus-visible {
outline: 3px solid var(--color-tcar17-acc);
outline-offset: 2px;
}
/* One rule decides which control set the user sees. */
@supports selector(::scroll-button(right)) {
.tcar17-fallback {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.tcar17-scroller {
scroll-behavior: auto;
}
.tcar17-scroller::scroll-button(left),
.tcar17-scroller::scroll-button(right) {
transition: none;
}
}
```Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Prev / Next Arrow Carousel with Native Scroll Buttons
Source: https://codefronts.com/snippets/tailwind-css-image-carousel/tailwind-prev-next-arrow-carousel-scroll-button/
Real previous/next buttons that page the gallery one slide at a time, generated by the browser via ::scroll-button() — focusable, auto-disabling at the ends, and needing no JavaScript. Anchor-link arrows sit underneath as the universal fallback.
## HTML
```html
<section class="tcar-17 w-full min-h-svh grid place-items-center bg-tcar17-bg font-tcar17-sans px-5 py-10" aria-label="Featured shots">
<div class="relative w-[min(620px,100%)]">
<ul tabindex="0" role="group" aria-label="Shots, use the arrow buttons or arrow keys" class="tcar17-scroller list-none flex overflow-x-auto m-0 p-0 rounded-2xl snap-x snap-mandatory scroll-smooth [scrollbar-width:none] shadow-[0_24px_50px_-34px_oklch(0.35_0.1_265/0.5)] focus-visible:outline-3 focus-visible:outline-tcar17-acc focus-visible:outline-offset-[3px]">
<li id="tcar-17-s1" class="flex-none basis-full snap-center aspect-[16/10] overflow-hidden"><img src="https://picsum.photos/seed/arrow-1/900/560" width="900" height="560" fetchpriority="high" decoding="async" alt="City skyline" class="size-full object-cover block"></li>
<li id="tcar-17-s2" class="flex-none basis-full snap-center aspect-[16/10] overflow-hidden"><img src="https://picsum.photos/seed/arrow-2/900/560" width="900" height="560" loading="lazy" decoding="async" alt="Desert highway" class="size-full object-cover block"></li>
<li id="tcar-17-s3" class="flex-none basis-full snap-center aspect-[16/10] overflow-hidden"><img src="https://picsum.photos/seed/arrow-3/900/560" width="900" height="560" loading="lazy" decoding="async" alt="Rainforest canopy" class="size-full object-cover block"></li>
<li id="tcar-17-s4" class="flex-none basis-full snap-center aspect-[16/10] overflow-hidden"><img src="https://picsum.photos/seed/arrow-4/900/560" width="900" height="560" loading="lazy" decoding="async" alt="Frozen lake" class="size-full object-cover block"></li>
</ul>
<nav aria-label="Slide navigation (fallback)" class="tcar17-fallback absolute inset-0 pointer-events-none">
<a href="#tcar-17-s1" aria-label="Previous" class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-auto size-11 flex items-center justify-center rounded-full bg-white text-tcar17-acc text-[26px] font-bold no-underline shadow-[0_8px_20px_-8px_oklch(0.35_0.1_265/0.6)] transition-[scale] duration-200 hover:scale-[1.08] focus-visible:outline-3 focus-visible:outline-tcar17-acc focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:hover:scale-100">‹</a>
<a href="#tcar-17-s4" aria-label="Next" class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-auto size-11 flex items-center justify-center rounded-full bg-white text-tcar17-acc text-[26px] font-bold no-underline shadow-[0_8px_20px_-8px_oklch(0.35_0.1_265/0.6)] transition-[scale] duration-200 hover:scale-[1.08] focus-visible:outline-3 focus-visible:outline-tcar17-acc focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:hover:scale-100">›</a>
</nav>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tcar17-bg: #eef2fb;
--color-tcar17-bg: oklch(0.95 0.015 265);
--color-tcar17-acc: #2563eb;
--color-tcar17-acc: oklch(0.52 0.2 262);
--font-tcar17-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}
.tcar-17 {
width: 100%;
min-height: 100svh;
}
.tcar-17 ::selection {
background: var(--color-tcar17-acc);
color: #fff;
}
.tcar17-scroller::-webkit-scrollbar {
display: none;
}
/* CSS Carousel scroll buttons: no utility form, so the whole control lives here. */
.tcar17-scroller::scroll-button(left),
.tcar17-scroller::scroll-button(right) {
position: absolute;
top: 50%;
translate: 0 -50%;
z-index: 3;
width: 44px;
height: 44px;
border: 0;
border-radius: 50%;
cursor: pointer;
background: #fff;
color: var(--color-tcar17-acc);
font-size: 22px;
font-weight: 700;
line-height: 1;
box-shadow: 0 8px 20px -8px oklch(0.35 0.1 265 / 0.6);
transition: scale .2s, opacity .2s;
}
.tcar17-scroller::scroll-button(left) {
content: "\2039";
left: 12px;
}
.tcar17-scroller::scroll-button(right) {
content: "\203A";
right: 12px;
}
.tcar17-scroller::scroll-button(left):hover,
.tcar17-scroller::scroll-button(right):hover {
scale: 1.08;
}
.tcar17-scroller::scroll-button(left):disabled,
.tcar17-scroller::scroll-button(right):disabled {
opacity: .35;
cursor: default;
}
.tcar17-scroller::scroll-button(left):focus-visible,
.tcar17-scroller::scroll-button(right):focus-visible {
outline: 3px solid var(--color-tcar17-acc);
outline-offset: 2px;
}
/* One rule decides which control set the user sees. */
@supports selector(::scroll-button(right)) {
.tcar17-fallback {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.tcar17-scroller {
scroll-behavior: auto;
}
.tcar17-scroller::scroll-button(left),
.tcar17-scroller::scroll-button(right) {
transition: none;
}
}
```How this works
The browser builds the buttons. ::scroll-button(left) and ::scroll-button(right) render genuine focusable buttons that page the snap scroller and disable themselves at each end — behaviour that has always required a script and an IntersectionObserver.
Two control sets, one visible. The markup also carries anchor arrows. @supports selector(::scroll-button(right)) hides them wherever the pseudo-element exists, so the user never sees four arrows and never sees none.
The fallback is the Tailwind part. Everything about the anchor arrows — size, shape, shadow, hover, focus ring — is utilities. Everything about the native buttons is CSS. The demo is a clean illustration of where the line sits.
Make it yours
- Glyphs: the
contentin the two::scroll-button()rules and the text of the two anchors. - Arrow position:
left-3/right-3on the fallback,left:12px/right:12pxin the pseudo rules. - Step size: slide width plus
snap-align. - Colour:
--color-tcar17-acc, read by both control sets.
Gotchas — read before shipping
::scroll-button()is Chromium 135+. Ship the fallback or most of the world gets a gallery with no visible controls.- A
::scroll-button()with emptycontenthas no accessible name. Give it a glyph, or anaria-labelviacontent: "\203A" / "Next". - Keep both control sets styled identically, or the fallback looks like a different component to the users who get it.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 135+ | n/a | n/a | 135+ |
::scroll-button() is Chromium 135+. The anchor-link arrows provide a universal fallback at Tailwind v4's usual floor, and @supports selector() picks between them.