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

live
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

npm install @lemonadejs/carousel
import Carousel from '@lemonadejs/carousel';

Three deployment forms, one component:

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.

PropTypeDefaultDescription
bindnumberTwo-way bound value. .set() fires onchange; plain assignment is silent. two-way current slide index
dataarrayslides: { image?, title?, description?, link? }
autoplaynumber0ms between automatic advances (0 = off)
loopbooleanfalsewrap next/prev past the edges
arrowsbooleantrueprev/next overlay buttons
dotsbooleantrueone 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

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:

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.