GalleryFree

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.

CSSTransformArrows

Updated

Hover or click the scene to interact

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

2 personnes · 24 m² · vue sur le lac

148 € / night

① Rooms of a lakeside hotel — photo band on the Rooms page

WhenThe visitor lands on the Rooms page and clicks › to move from the Shore room to the Jetty room; under the band, the booking line shows the guests, size and price of the room on screen.
WhyOne room at a time, large and uncluttered, is what sells a night in a hotel. The sideways slide keeps the feeling of a tour (you move forward through the rooms) and the arrows sit on the photo, where the eye already is.
SettingsFull-width band: --bsld-width:100%, --bsld-height:100%, --bsld-radius:0, transparent frame with no padding (--bsld-stage-bg:transparent, --bsld-stage-padding:0), dark arrows --bsld-arrow-bg:rgba(36,50,58,.42). Photos as <img>, caption moved to the bottom left by the page CSS. The price follows the slide through the inert attribute the effect sets on the others.
Trajolle
1 / 3

② Three welcome screens — first launch of a neighbourhood car-sharing app

WhenOn first launch, a window introduces the app in three steps; each click on › slides in the next step, the counter goes from 1 / 3 to 3 / 3 and the "Let's go" button lights up on the last one.
WhyUsers move at their own pace and can go back with ‹: nothing scrolls without them, which matters for text to be read. The same component as the photo band works here as a step-by-step, with a white background and an illustration instead of a full-frame photo.
SettingsWhite slides laid out in a column (image, title, text), green arrows: --bsld-arrow-bg:#e3f2ec and icon stroke #1f7a5c in the scene CSS. Height taken from the window (--bsld-height:100%). The counter and progress bars read the active slide; the effect itself is unchanged.
Mottebrune A blog about words and books

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

③ Forgotten word of the day — a box in the sidebar of a reading blog

WhenNext to an article, a small card shows a rare word and its definition; the reader puts focus on an arrow and browses the previous days with ← and →.
WhyAt this size the carousel is a layout detail that does not steal the show from the article. The keyboard is enough to go through it, and wrapping back to the first card after the last avoids a greyed-out button at the end of the list.
SettingsCard 132 px high: --bsld-height:132px, --bsld-ratio:auto, --bsld-radius:12px, light background and 28 px arrows restyled in the scene CSS. Slides hold text only (word and definition inside .bsld-caption).

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) with aria-roledescription="carousel"; the script gives each slide role="group", aria-roledescription="slide" and a "2 of 5" name.
  • Hidden really means hidden: slides outside the frame get inert and aria-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: reduce the 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).

Chrome 102+✓ Full (measured)
Firefox 112+✓ Full (measured)
Safari 15.5+✓ Full (measured in WebKit)
Edge 102+✓ Full (Chromium engine)
iOS Safari 15.5+✓ Not checked on a real device
Android Chrome 102+✓ Full (Chromium engine)

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.

HTML
<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>
CSS
.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;
  }
}
JavaScript (fx-0161)
(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 / propertyDefaultEffect
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.