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
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.
| 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
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.