Framework-agnostic, position-based carousel and slider with responsive layouts, loop mode, cancellable transitions, typed events, touch gestures, accessibility, and composable plugins. Published as an ESM package for modern browsers.
Live demo · npm · Русская версия
- Responsive single- and multi-slide layouts with fixed or automatic scroll steps.
- Loop mode, cancellable transitions, keyboard control, touch and mouse gestures.
- Arrows, dots, thumbnails, autoplay, lazy loading, swipe, and parallax plugins.
- Framework-independent lifecycle, typed events, and TypeScript declarations.
npm install @shelamkoff/carousel @shelamkoff/event-busImport the required stylesheet once in the application:
import '@shelamkoff/carousel/styles.css'carouselStylesUrl is also exported when the host needs to create its own <link> element.
<div id="gallery"></div>import {
Carousel,
createArrows,
createDots,
createKeyboard,
createSwipe,
} from '@shelamkoff/carousel'
import '@shelamkoff/carousel/styles.css'
const carousel = new Carousel('#gallery', [
{
content: () => Object.assign(document.createElement('img'), {
src: '/photos/forest.jpg',
alt: 'Forest',
}),
thumb: '/photos/forest-thumb.jpg',
},
{ content: '<p>Trusted application markup</p>' },
], {
loop: true,
gap: 16,
plugins: [
createArrows(),
createDots(),
createKeyboard(),
createSwipe({ mouse: true }),
],
})
const unsubscribe = carousel.on('slide:change', ({ index, slide }) => {
console.log(index, slide.meta)
})
carousel.next()
// Later:
unsubscribe()
carousel.destroy()The container may be an HTMLElement or a CSS selector. A selector that does not resolve to an element causes the constructor to throw.
interface SlideData {
content: string | HTMLElement | (() => HTMLElement)
thumb?: string
lazy?: boolean
meta?: Record<string, unknown>
}- A render function is the safest choice for dynamic or untrusted data.
- An
HTMLElementis moved into the carousel; it is not cloned. - A string is assigned as HTML and is therefore trusted developer input.
thumbis consumed by the thumbnails plugin,lazyby lazy loading, andmetaremains application-owned metadata.
| Option | Type | Default | Purpose |
|---|---|---|---|
slidesPerView |
number |
1 |
Number of visible slides. Positive values are required. |
slidesPerScroll |
number | 'auto' |
1 |
Navigation step. 'auto' uses the resolved visible count. |
gap |
number |
0 |
Gap between slides in CSS pixels. |
transition |
'slide' | 'fade' |
'slide' |
Movement strategy. Fade mode displays one slide at a time. |
transitionDuration |
number |
400 |
Transition duration in milliseconds. |
loop |
boolean |
false |
Enables circular navigation using loop clones. |
startIndex |
number |
0 |
Initial logical slide index. Used only during construction. |
breakpoints |
Record<number, Partial<CarouselResponsiveOptions>> |
none | Options applied when the viewport is at least the numeric width. |
plugins |
CarouselPlugin[] |
[] |
Plugins installed during construction. |
Breakpoint values override base options. The carousel preserves the current logical slide, cancels obsolete animation work, and rebuilds layout when the active breakpoint changes.
const carousel = new Carousel('#gallery', slides, {
slidesPerView: 1,
slidesPerScroll: 'auto',
breakpoints: {
640: { slidesPerView: 2, gap: 12 },
1024: { slidesPerView: 4, gap: 20 },
},
})| Method | Result | Description |
|---|---|---|
use(plugin) |
this |
Installs a plugin. Plugin names must be unique. |
getPlugin(name) |
plugin or undefined |
Returns the installed public plugin object. |
next() / prev() |
void |
Moves by the resolved scroll step. |
goTo(index) |
void |
Navigates to a logical slide index. |
getIndex() |
number |
Current internal/render index. Prefer getRealIndex() in loop mode. |
getRealIndex() |
number |
Current logical index in the source slide array. |
getSlide(index?) |
slide or null |
Returns a logical slide; omitting the index returns the current slide. |
getSlides() |
SlideData[] |
Returns a defensive array copy of slides. |
addSlide(slide, index?) |
void |
Adds a slide and rebuilds the layout. |
removeSlide(index) |
void |
Removes a logical slide and keeps the remaining index valid. |
update(options) |
void |
Updates layout and behavior. plugins and startIndex are intentionally rejected. |
on(event, handler) |
unsubscribe function | Subscribes to an event. |
off(event, handler) |
void |
Removes the exact handler. |
once(event, handler) |
unsubscribe function | Subscribes for one emission. |
destroy() |
void |
Cancels animations, destroys plugins, removes generated DOM and listeners. |
Do not call methods after destroy(). Create a new instance if the host needs to mount the carousel again.
| Event | Payload |
|---|---|
slide:beforeChange |
{ fromIndex, toIndex } |
slide:change |
{ index, prevIndex, slide } |
transition:start |
{ fromIndex, toIndex } |
transition:end |
{ index } |
resize |
{ slidesPerView } |
autoplay:start / autoplay:stop |
no payload |
slide:lazyload |
{ index, element } |
slide:error |
{ index, error } |
destroy |
no payload |
Rendering and lazy-load failures are isolated to the affected slide and reported through slide:error.
- Arrows — previous and next buttons; edge hiding is enabled by default.
- Dots — clickable pagination.
- Thumbnails — bottom or side thumbnail navigation.
- Keyboard — focus-scoped arrow keys by default; document-wide handling is opt-in.
- Swipe — touch, pointer, resistance, and optional mouse drag.
- Autoplay — interval playback with hover and interaction pausing.
- Lazy load — observer-based image loading and configurable preloading.
- Parallax — position-driven visual offset.
Install a plugin in plugins or with use() before destroying the carousel. A stateful plugin instance may belong to only one live carousel; create a fresh plugin object for every concurrent carousel.
A plugin has a unique name, install(context), and optional destroy():
export function createProgressPlugin() {
let element = null
return {
name: 'progress',
install(context) {
element = document.createElement('output')
element.className = 'carousel-progress'
element.setAttribute('aria-live', 'polite')
const render = () => {
element.textContent = `${context.getRealIndex() + 1} / ${context.getSlideCount()}`
}
context.getRoot()?.append(element)
context.on('slide:change', render)
context.on('resize', render)
render()
},
destroy() {
element?.remove()
element = null
},
}
}The frozen context exposes owned event subscriptions, navigation commands, read-only state and resolved options, live DOM getters, position methods for gesture/effect plugins, and refreshLayout(). Context subscriptions are removed automatically. The plugin must still release its own DOM, global listeners, observers, timers, animation frames, object URLs, and third-party instances in destroy().
If install() throws, the carousel rolls back context subscriptions and invokes the plugin cleanup. Make destroy() safe after partial installation.
String slide.content and custom arrow HTML are inserted as HTML. They must contain trusted developer-controlled markup. For user-controlled content, build an element with textContent or sanitize the value before passing it to the carousel.
Open the live demo to try responsive layouts, loop and autoplay, fade, thumbnails, parallax, custom themes, and fixed scroll steps.
To run the same demo locally:
npm install
npm run demoOpen http://127.0.0.1:4173/demo.html.
@shelamkoff/carousel— JavaScript API and TypeScript declarations.@shelamkoff/carousel/styles.css— required carousel styles.@shelamkoff/carousel/package.json— package metadata.
MIT.