---
title: "JavaScript Carousel"
description: "The LemonadeJS Carousel block for JavaScript: Swipeable slide carousel. Contract-verified, framework-agnostic, zero dependencies, with a live example."
source: https://lemonadejs.com/docs/plugins/carousel/
---

# JavaScript Carousel

`@lemonadejs/carousel` · ✓ 14 contract checks · framework-agnostic · zero dependencies

(new in v6)

SINGLE-FILE component: the CSS ships INSIDE the template via the v6
component-owned `<style>` hoisting — no style.css anywhere. The engine
lifts the `<style>` at parse time and injects it into document.head ONCE
per template, however many instances mount. Hoisted styles are global,
so every selector is prefixed lm-carousel-*.

Model: slides sit side by side in a flex strip (each 100% wide); the
position is a translateX on the strip — `-index·100% + dragPx` — fully
deterministic (jsdom has no layout). The snap animation is one CSS
transition, suspended while dragging via the lm-carousel-dragging
class. Slides and dots are keyed by slide identity.

Gestures (the llms.txt listen() pattern): pointer-down arms document
mousemove/touchmove/mouseup/touchend (+ keydown for Escape) per
gesture with ONE persistent release; pointer-up COMMITS — a drag past
25% of the viewport width goes to the next/prev slide, anything less
snaps back. Escape cancels the drag in flight. A mid-drag unmount
releases everything (onUnmount → release).

Autoplay: setInterval armed when autoplay > 0, re-armed live on prop
change (subscribe), PAUSED while hovering, focused (focusin/focusout)
or dragging, cleared on unmount. When autoplay is active a labelled
pause/play toggle renders BEFORE the slides (WAI-ARIA APG carousel
pattern); the toggle itself never pauses rotation on focus, so "play"
resumes immediately. Autoplay always wraps (rewinds to 0 after the
last slide), even when loop=false — loop only governs user navigation
at the edges.

Contract: bind (current index, two-way; set → onchange), data (slides:
{ image?, title?, description?, link? } — only provided fields
render), autoplay (ms, 0 = off), loop, arrows, dots, onchange(index);
api { next, prev, goto }. Keyboard: ArrowLeft/ArrowRight on the
focused region. ARIA: aria-roledescription carousel/slide, off-screen
slides aria-hidden AND inert (their links leave the tab order), slide
labels carry the title, and the track is aria-live=polite only while
not auto-rotating.

## Example

<!--example-->

```js
import { html } from 'lemonadejs';
import Carousel from '@lemonadejs/carousel';

const slides = [
    { image: 'https://picsum.photos/id/1015/800/360', title: 'River bend', description: 'Drag, swipe or use the arrow keys.' },
    { image: 'https://picsum.photos/id/1016/800/360', title: 'Canyon', description: 'Autoplay pauses while you hover.' },
    { image: 'https://picsum.photos/id/1018/800/360', title: 'Peaks', description: 'Loop wraps from the last slide back to the first.' },
];

const App = (props, { state }) => {
    const current = state(0);

    return html`<div style="max-width: 640px;">
        <${Carousel} bind="${current}" data="${slides}" autoplay="4000" loop />
        <p>Slide <b>${() => current.value + 1}</b> of ${slides.length}: ${() => slides[current.value].title}</p>
    </div>`;
};
```

## Installation

```bash
npm install @lemonadejs/carousel
```

```js
import Carousel from '@lemonadejs/carousel';
```

Three deployment forms, one component:

```js
html`<${Carousel} />`                       // by value (no registration)
setComponents({ Carousel });               // then <Carousel /> by name anywhere
createWebComponent(Carousel);              // <lm-carousel> in plain HTML/any framework
```

## Props

Every declared prop arrives as a **live state** — pass a value for a snapshot or a
state for a two-way live wire. Attribute strings are coerced to the declared type.

| Prop | Type | Default | Description |
|---|---|---|---|
| `bind` | number | — | Two-way bound value. `.set()` fires `onchange`; plain assignment is silent. two-way current slide index |
| `data` | array | — | slides: { image?, title?, description?, link? } |
| `autoplay` | number | `0` | ms between automatic advances (0 = off) |
| `loop` | boolean | `false` | wrap next/prev past the edges |
| `arrows` | boolean | `true` | prev/next overlay buttons |
| `dots` | boolean | `true` | one dot per slide, clickable |

## Events

All event names are lowercase (the platform convention — LJS-305 warns otherwise).

- `onchange` — (index) on user/component-initiated changes

## API

```js
import { ref } from 'lemonadejs';
const carousel = ref();
html`<${Carousel} ref="${carousel}" />`;
// carousel.current.next(...)  ·  carousel.current.prev(...)  ·  carousel.current.goto(...)
```

- `next()`
- `prev()`
- `goto()`

## Styling

All classes follow the `lm-carousel-*` convention; visual variants are `data-*`
attributes on the root. Override freely — there is no styling engine to fight.

## Contract

The machine-readable schema ships with the package:

```js
import contract from '@lemonadejs/carousel/contract.json';
```

`verify.json` carries the conformance proof produced by `verify(Carousel)`.

Looking for the v5 plugin? See the [archived v5 documentation](/docs/v5/plugins/).