Basic Slider
Arrow carousel: the track slides from one image to the next on click, on the ← → keys or with a swipe, and starts over after the last one.
Updated
This effect is free — the Gallery category contains 44 effects total, including 6 free. Effect.Labs has 840 vanilla effects. Explore the category →
Usage examples
READING NOTES
Why old novels feel so slow
Long descriptions were the cinema of their time: readers had never seen the sea, the city or the mountain, so the author showed them everything, line after line.
Today the picture comes first and the text has to catch up with it.
“Describing was travelling on the reader's behalf.”
8 min read · 3 comments
How it works
The .bsld-slide elements sit side by side in .bsld-track, each one 100% as wide as the .bsld-carousel frame. Changing slide means setting translateX(-100%), -200%… on the track; the 0.5 s ease-out CSS transition does the sliding. After the fifth slide, the next button brings back the first one: the track runs backwards in a single move, it does not loop endlessly.
Three ways to navigate: the ‹ and › buttons (.bsld-prev, .bsld-next), the ← and → keys while focus is inside the carousel (on one of the arrows, for instance), and a horizontal drag of more than 50 px, by finger or mouse. Dragging goes through Pointer Events; a mostly vertical move is left to the page, which scrolls normally on mobile. Nothing moves on its own.
The frame follows its container's width up to 640 px and keeps a 16/10 ratio (aspect-ratio). For your photos, put an <img> tag inside the slide: it is cropped with object-fit: cover and the .bsld-caption label sits on top. The purple gradients are only there for the demo.
Each .bsld-carousel is set up on its own: two carousels on the same page do not drive each other, no variable is put on window, and a script pasted twice wires nothing twice (data-bsld-ready marker).
Accessibility
- Carousel roles: the frame is a named
<div role="region">(aria-label) witharia-roledescription="carousel"; the script gives each sliderole="group",aria-roledescription="slide"and a "2 of 5" name. - Hidden really means hidden: slides outside the frame get
inertandaria-hidden, so a link placed on a hidden image cannot be reached with Tab. - Change announced: an invisible
aria-live="polite"paragraph says "slide 3 of 5" after each click, key press or swipe; nothing is announced on load. - Named arrows: real
<button type="button">elements, "Previous slide" and "Next slide", with a white keyboard focus ring and SVG icons hidden from screen readers. - Reduced motion: under
prefers-reduced-motion: reducethe track jumps instead of sliding; arrows, keys and swiping still work.
Browser compatibility
About 3.5 KB of dependency-free JavaScript. Measured on 08/11/2026 in Chromium, Firefox and WebKit (Playwright): two independent copies, script pasted twice, Tab then Enter, ← → keys, finger swipe at 390 px (Chromium) and mouse drag (Firefox, WebKit), reduced motion, no errors. Minimum versions from @mdn/browser-compat-data 8.1.8: inert sets the floor (Chrome 102, Firefox 112, Safari 15.5).
Before inert (Chrome 88-101, Firefox 89-111, Safari 15-15.4) everything works, but a link placed in a hidden slide can still be reached with Tab. Before Safari 15 (and before Chrome 88, Firefox 89), aspect-ratio is missing: set a height with --bsld-height. Without JavaScript, the first slide stays visible and the arrows do nothing.
The code
Copy the three blocks into your page. No dependencies.
<div class="fx-0161-root">
<div class="bsld-carousel" role="region" aria-roledescription="carousel" aria-label="Slideshow" data-slide="slide" data-of="of">
<div class="bsld-track">
<div class="bsld-slide"><span class="bsld-caption">Slide 1</span></div>
<div class="bsld-slide"><span class="bsld-caption">Slide 2</span></div>
<div class="bsld-slide"><span class="bsld-caption">Slide 3</span></div>
<div class="bsld-slide"><span class="bsld-caption">Slide 4</span></div>
<div class="bsld-slide"><span class="bsld-caption">Slide 5</span></div>
</div>
<button type="button" class="bsld-arrow bsld-prev" aria-label="Previous slide">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M15 18l-6-6 6-6"/></svg>
</button>
<button type="button" class="bsld-arrow bsld-next" aria-label="Next slide">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 18l6-6-6-6"/></svg>
</button>
</div>
</div>
.fx-0161-root,
.fx-0161-root *,
.fx-0161-root *::before,
.fx-0161-root *::after {
box-sizing: border-box;
}
/* Demo frame: background, padding and minimum height are adjustable */
.fx-0161-root {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: var(--bsld-stage-min-height, 250px);
padding: var(--bsld-stage-padding, 25px 16px);
background: var(--bsld-stage-bg, #1a1a24);
color: #fff;
font-family: sans-serif;
}
/* The carousel takes its container's width, up to --bsld-width; height = width × ratio */
.bsld-carousel {
position: relative;
width: 100%;
max-width: var(--bsld-width, 640px);
height: var(--bsld-height, auto);
aspect-ratio: var(--bsld-ratio, 16 / 10);
overflow: hidden;
overflow: clip; /* the frame cannot scroll by itself (focus, scrollIntoView) */
border-radius: var(--bsld-radius, 1rem);
background: #12121a;
}
.bsld-track {
display: flex;
height: 100%;
transition: transform var(--bsld-duration, 0.5s) ease-out;
touch-action: pan-y; /* a vertical finger move scrolls the page, a horizontal one moves the carousel */
user-select: none;
-webkit-user-select: none;
}
.bsld-slide {
position: relative;
flex: 0 0 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
/* Your photos: an <img> inside the slide, cropped to fill it */
.bsld-slide > img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
-webkit-user-drag: none;
}
.bsld-caption {
position: relative;
font-weight: 600;
font-size: 1.2rem;
color: #fff;
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.35);
}
/* Demo backgrounds (covered by your images) */
.bsld-slide:nth-child(1) { background: linear-gradient(135deg, #6366f1, #8b5cf6); }
.bsld-slide:nth-child(2) { background: linear-gradient(135deg, #8b5cf6, #d946ef); }
.bsld-slide:nth-child(3) { background: linear-gradient(135deg, #d946ef, #f43f5e); }
.bsld-slide:nth-child(4) { background: linear-gradient(135deg, #f43f5e, #f97316); }
.bsld-slide:nth-child(5) { background: linear-gradient(135deg, #f97316, #eab308); }
.bsld-arrow {
position: absolute;
top: 50%;
z-index: 10;
width: 36px;
height: 36px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
transform: translateY(-50%);
background: var(--bsld-arrow-bg, rgba(255, 255, 255, 0.2));
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
cursor: pointer;
transition: background 0.3s ease, transform 0.3s ease;
}
.bsld-arrow:hover {
background: rgba(255, 255, 255, 0.4);
transform: translateY(-50%) scale(1.1);
}
.bsld-arrow:focus-visible {
outline: 2px solid #fff;
outline-offset: 2px;
}
.bsld-arrow svg {
width: 18px;
height: 18px;
stroke: #fff;
stroke-width: 2;
fill: none;
}
.bsld-prev { left: 10px; }
.bsld-next { right: 10px; }
/* Text read by screen readers only */
.bsld-status {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
/* Reduced motion: the slide still changes, without the sliding */
@media (prefers-reduced-motion: reduce) {
.bsld-track,
.bsld-arrow {
transition: none;
}
}
(function () {
// Basic Slider: the track slides from one slide to the next (arrows, ← → keys, finger or mouse drag).
// Each .bsld-carousel on the page gets its own instance; nothing is declared on window.
function init(car) {
if (car.hasAttribute('data-bsld-ready')) return; // script pasted twice: nothing is wired twice
var track = car.querySelector('.bsld-track');
var slides = track ? track.querySelectorAll('.bsld-slide') : [];
if (!slides.length) return;
car.setAttribute('data-bsld-ready', '');
var prev = car.querySelector('.bsld-prev');
var next = car.querySelector('.bsld-next');
var word = car.getAttribute('data-slide') || 'slide';
var of = car.getAttribute('data-of') || 'of';
var n = slides.length;
var index = 0;
// announces "slide 2 of 5" to screen readers after each change
var status = document.createElement('p');
status.className = 'bsld-status';
status.setAttribute('aria-live', 'polite');
car.appendChild(status);
for (var i = 0; i < n; i++) {
slides[i].setAttribute('role', 'group');
slides[i].setAttribute('aria-roledescription', word);
slides[i].setAttribute('aria-label', (i + 1) + ' ' + of + ' ' + n);
}
function go(k, announce) {
index = ((k % n) + n) % n; // loops: after the last slide comes the first
track.style.transform = 'translateX(' + (-100 * index) + '%)';
for (var i = 0; i < n; i++) {
// hidden slides: out of the Tab order and of screen readers
if (i === index) {
slides[i].removeAttribute('inert');
slides[i].removeAttribute('aria-hidden');
} else {
slides[i].setAttribute('inert', '');
slides[i].setAttribute('aria-hidden', 'true');
}
}
if (announce) status.textContent = (car.getAttribute('data-slide') || word) + ' ' + (index + 1) + ' ' + (car.getAttribute('data-of') || of) + ' ' + n;
}
if (prev) prev.addEventListener('click', function () { go(index - 1, true); });
if (next) next.addEventListener('click', function () { go(index + 1, true); });
car.addEventListener('keydown', function (e) {
if (/^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName) || e.target.isContentEditable) return;
if (e.key === 'ArrowLeft') { e.preventDefault(); go(index - 1, true); }
if (e.key === 'ArrowRight') { e.preventDefault(); go(index + 1, true); }
});
// Drag (Pointer Events: finger, mouse, pen): more than 50 px horizontally changes the slide
var x0 = null, y0 = 0, dragged = false;
track.addEventListener('pointerdown', function (e) {
if (e.pointerType === 'mouse' && e.button !== 0) return;
x0 = e.clientX; y0 = e.clientY; dragged = false;
});
track.addEventListener('pointerup', function (e) {
if (x0 === null) return;
var dx = e.clientX - x0, dy = e.clientY - y0;
x0 = null;
if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy)) {
dragged = true;
go(index + (dx < 0 ? 1 : -1), true);
}
});
track.addEventListener('pointercancel', function () { x0 = null; });
// a mouse drag must not open a link placed on the slide
track.addEventListener('click', function (e) {
if (dragged) { e.preventDefault(); e.stopPropagation(); dragged = false; }
}, true);
track.addEventListener('dragstart', function (e) { e.preventDefault(); });
go(0, false);
}
Array.prototype.forEach.call(document.querySelectorAll('.bsld-carousel'), init);
})();
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Maximum width (CSS — --bsld-width on .fx-0161-root) |
640px | The carousel takes all the available width up to this value. 100% for an unlimited band. |
Ratio (CSS — --bsld-ratio) |
16 / 10 | Height computed from the width. 4 / 3 for product photos, 21 / 9 for a banner. |
Fixed height (CSS — --bsld-height) |
auto | A value (320px, 100%) replaces the ratio-based height; handy inside a block with an imposed height. |
Slide duration (CSS — --bsld-duration) |
0.5s | Length of the track transition, ease-out. No effect under reduced motion. |
Corners and arrow background (CSS — --bsld-radius, --bsld-arrow-bg) |
1rem · white at 20% | On light photos, darken the arrows: --bsld-arrow-bg: rgba(0,0,0,.45). |
Demo frame (CSS — --bsld-stage-bg, --bsld-stage-padding, --bsld-stage-min-height) |
#1a1a24 · 25px 16px · 250px | Background and spacing around the carousel. In your page: transparent, 0 and 0. |
Spoken texts (HTML — aria-label, data-slide, data-of on .bsld-carousel) |
Slideshow · slide · of | Name of the carousel and words of the "slide 2 of 5" announcement. Arrow labels are changed on the buttons. |
Your images (HTML — <img> inside .bsld-slide) |
demo gradients | The image covers the slide; keep an alt that describes the photo. As many slides as you like: the script counts them. |
FAQ
Can the carousel scroll automatically?
Not in this code: it only moves when someone acts. If you add a timer that clicks .bsld-next, provide a pause button, stop on hover and focus, and do nothing under prefers-reduced-motion: motion lasting more than five seconds without a pause is not accessible.
Why does going from the last slide back to the first run past all the others?
The track simply returns to translateX(0), so the slide passes back over the slides in between. To chain without that rewind you need copies at both ends of the track, which is what the Infinite Loop effect in the same category does.
Does the slide stay aligned when the window is resized?
Yes. The position is a percentage of the track width (translateX(-200%) for the third slide), not pixels: when the window or the column changes width, the slide on screen stays in place with no recalculation and no ResizeObserver.