
Swipi is a headless JavaScript carousel engine that handles dragging, momentum, snapping, looping, and autoplay. The Vanilla JS package exposes the carousel through createSwipi() while your application defines the HTML, CSS, controls, and accessibility markup.
Note that swipi includes no stylesheet. It measures the dimensions produced by your CSS, which keeps slide widths, responsive layouts, spacing, arrows, dots, and visual states under application control.
React, Vue, Svelte, and Angular adapters use the same carousel engine. The implementation below uses the dependency-free @midstem/swipi package for plain JavaScript.
Features
- Dependency-free Vanilla JavaScript.
- Headless HTML and CSS model.
- Mouse and touch dragging with momentum.
- Snap-based and drag-free movement.
- Infinite looping and autoplay.
- Horizontal and vertical layouts.
- Responsive slide sizing through CSS.
- Programmatic navigation and observable carousel state.
- Reduced-motion handling for Swipi track movement.
How To Use It
Browser
Import the ESM package directly from a CDN.
<script type="module">
import { createSwipi } from 'https://cdn.jsdelivr.net/npm/@midstem/[email protected]/+esm';
</script>
npm
You can also install the core package with NPM and import createSwipi into your JS.
npm install @midstem/swipi
import { createSwipi } from '@midstem/swipi';
Basic Usage
Swipi requires a viewport, one track inside that viewport, and slides as direct children of the track. The viewport clips the moving track. Slide widths determine how many items appear at once and where each snap position lands.
The example below uses two visible cards, a 12px gap, drag gestures, and previous/next buttons. subscribe() keeps the button states synchronized with carousel navigation.
<div class="demo-carousel">
<div class="carousel-viewport">
<div class="carousel-track">
<article class="carousel-slide">Project One</article>
<article class="carousel-slide">Project Two</article>
<article class="carousel-slide">Project Three</article>
<article class="carousel-slide">Project Four</article>
</div>
</div>
<div class="carousel-controls">
<button type="button" class="carousel-prev" aria-label="Previous slide">
Previous
</button>
<button type="button" class="carousel-next" aria-label="Next slide">
Next
</button>
</div>
</div>
<style>
.carousel-viewport {
overflow: hidden;
touch-action: pan-y;
}
.carousel-track {
display: flex;
margin-left: -12px;
user-select: none;
}
.carousel-slide {
box-sizing: border-box;
flex: 0 0 50%;
min-width: 0;
padding-left: 12px;
}
.carousel-controls {
display: flex;
gap: 8px;
margin-top: 16px;
}
</style>
<script type="module">
import { createSwipi } from 'https://cdn.jsdelivr.net/npm/@midstem/[email protected]/+esm';
const viewport = document.querySelector('.carousel-viewport');
const prevButton = document.querySelector('.carousel-prev');
const nextButton = document.querySelector('.carousel-next');
const carousel = createSwipi(viewport);
function updateControls() {
const state = carousel.getSnapshot();
prevButton.disabled = !state.canScrollPrev;
nextButton.disabled = !state.canScrollNext;
}
prevButton.addEventListener('click', function () {
carousel.scrollPrev();
});
nextButton.addEventListener('click', function () {
carousel.scrollNext();
});
carousel.subscribe(updateControls);
updateControls();
</script>
Responsive Slide Counts
Swipi reads the rendered slide geometry. Media queries can change flex-basis, and the carousel recalculates its snap positions after the dimensions change.
A gap-only breakpoint needs extra attention. Changing slide padding alone can leave the measured slide box at the same width. Use spaceBetween for a gap that changes independently from the slide count.
This CSS displays three slides above 800px, two below 800px, and one below 580px.
.carousel-slide {
box-sizing: border-box;
flex: 0 0 calc(100% / 3);
min-width: 0;
padding-left: 24px;
}
@media (max-width: 800px) {
.carousel-slide {
flex-basis: 50%;
}
}
@media (max-width: 580px) {
.carousel-slide {
flex-basis: 100%;
}
}
Container Queries
Carousel sizing does not depend on viewport width. A component inside a resizable sidebar can use a container query for the same slide-sizing rule.
.carousel-wrapper {
container-type: inline-size;
}
@container (max-width: 640px) {
.carousel-slide {
flex-basis: 100%;
}
}
Looping and Autoplay
loop keeps navigation active across the complete slide set. Autoplay advances every autoplaySpeed milliseconds, and animationSpeed controls programmed movement between snaps.
Autoplay reaches the final snap and stops when loop is false. A carousel that moves automatically for more than five seconds should expose a visible pause control. respectReducedMotion removes Swipi’s track travel when the operating system requests reduced motion.
const carousel = createSwipi(viewport, {
loop: true,
autoplay: true,
autoplaySpeed: 3500,
animationSpeed: 500,
respectReducedMotion: true
});
Drag-Free Scrolling
Normal dragging resolves to a snap position. dragFree: true retains momentum and permits the track to stop between snaps.
const carousel = createSwipi(viewport, {
dragFree: true
});
Vertical Carousel
axis: 'y' changes the movement direction to vertical. The viewport needs an explicit height, the track uses a column layout, the gap moves to the vertical axis, and touch-action changes to pan-x.
const carousel = createSwipi(viewport, {
axis: 'y'
});
.carousel-viewport {
height: 360px;
overflow: hidden;
touch-action: pan-x;
}
.carousel-track {
display: flex;
flex-direction: column;
height: 100%;
margin-top: -12px;
user-select: none;
}
.carousel-slide {
box-sizing: border-box;
flex: 0 0 50%;
min-height: 0;
padding-top: 12px;
}
All Configuration Options
axis('x' | 'y'): Sets horizontal or vertical movement. The default is'x'.loop(boolean): Keeps navigation moving through the slide set when enough content exists to overflow the viewport. The default isfalse.dragFree(boolean): Keeps drag momentum and permits the track to rest between snap positions. The default isfalse.autoplay(boolean): Advances the carousel automatically. The default isfalse.autoplaySpeed(number): Sets the autoplay interval in milliseconds. The default is4000.animationSpeed(number): Sets the duration of programmed carousel movement in milliseconds. The default is300.respectReducedMotion(boolean): Watchesprefers-reduced-motionand removes programmed track travel when reduced motion is active. The default isfalse.startIndex(number): Sets the initial snap index. The default is0.slideWidth(number): Writes a pixel value to--swipi-slide-width. CSS must read the property before it affects layout.spaceBetween(number): Writes a pixel value to--swipi-slide-gapand signals Swipi to measure the layout again.onSelect(function): Receives the full navigable state whenever carousel state changes.onChange(function): Receives the previous, current, and next indexes when the selected index changes.
onChange Event
Use onChange for captions, analytics, counters, or other code that should run only after the selected index changes.
const carousel = createSwipi(viewport, {
onChange: function ({ prev, current, next }) {
console.log('Previous:', prev);
console.log('Current:', current);
console.log('Next:', next);
}
});
onSelect Event
Use onSelect when application UI depends on the complete navigable state.
const carousel = createSwipi(viewport, {
onSelect: function (state) {
console.log(state.selectedIndex);
console.log(state.snapCount);
console.log(state.canScrollPrev);
console.log(state.canScrollNext);
}
});
API Methods
The Vanilla JS instance exposes navigation, state, live configuration, measurement, synchronization, and cleanup methods.
// Move to the next snap.
carousel.scrollNext();
// Move to the previous snap.
carousel.scrollPrev();
// Move to snap index 3.
carousel.scrollTo(3);
// Read the current carousel state.
const state = carousel.getSnapshot();
// Run a listener after state changes.
// The returned function removes the subscription.
const unsubscribe = carousel.subscribe(updateControls);
// Change configuration on a live carousel.
carousel.update({
autoplay: false,
loop: true
});
// Measure the carousel after an unobserved layout change.
carousel.measure();
// Reapply the current transform after slide rendering changes.
carousel.sync();
// Remove listeners, observers, and carousel behavior.
carousel.destroy();
`resolveOptions()`
resolveOptions() is an exported helper for code that needs Swipi’s resolved defaults.
import {
createSwipi,
resolveOptions
} from '@midstem/swipi';
const settings = resolveOptions({
loop: true,
autoplay: true
});
Reading Carousel State
selectedIndex: Current snap index.snapCount: Number of available snap positions.slidesCount: Number of slides in the track.hasOverflow: Reports whether the content extends beyond the viewport.canScrollNext: Reports whether forward navigation is available.canScrollPrev: Reports whether backward navigation is available.
const counter = document.querySelector('.carousel-counter');
function updateCounter() {
const state = carousel.getSnapshot();
counter.textContent =
`${state.selectedIndex + 1} / ${state.snapCount}`;
}
carousel.subscribe(updateCounter);
updateCounter();
CSS Variables
slideWidth and spaceBetween write two optional custom properties onto the track. Swipi does not use these properties to calculate CSS itself. The stylesheet must read them.
The example below uses both values to define fixed slide content width and spacing.
--swipi-slide-width: Pixel value written byslideWidth.--swipi-slide-gap: Pixel value written byspaceBetween.
const carousel = createSwipi(viewport, {
slideWidth: 280,
spaceBetween: 16
});
.carousel-track {
display: flex;
margin-left: calc(-1 * var(--swipi-slide-gap, 0px));
}
.carousel-slide {
box-sizing: border-box;
flex: 0 0 calc(
var(--swipi-slide-width, 280px) +
var(--swipi-slide-gap, 0px)
);
min-width: 0;
padding-left: var(--swipi-slide-gap, 0px);
}
Keyboard Controls and Accessibility
Swipi creates no accessibility attributes or text. Application markup owns the carousel role, accessible name, slide labels, arrow labels, dot state, keyboard handling, and live-region announcement.
Custom fades, parallax effects, dot transitions, and other CSS animation need their own prefers-reduced-motion rules. respectReducedMotion controls Swipi’s track movement.
The example below adds viewport keyboard navigation and a live region.
<div
class="carousel-viewport"
tabindex="0"
role="group"
aria-roledescription="carousel"
aria-label="Featured projects"
>
<div class="carousel-track">
<article
class="carousel-slide"
role="group"
aria-roledescription="slide"
aria-label="1 of 3"
>
Project One
</article>
<article
class="carousel-slide"
role="group"
aria-roledescription="slide"
aria-label="2 of 3"
>
Project Two
</article>
<article
class="carousel-slide"
role="group"
aria-roledescription="slide"
aria-label="3 of 3"
>
Project Three
</article>
</div>
</div>
<span class="carousel-status" aria-live="polite" aria-atomic="true"></span>
<script>
const status = document.querySelector('.carousel-status');
viewport.addEventListener('keydown', function (event) {
if (event.key === 'ArrowLeft') {
carousel.scrollPrev();
}
if (event.key === 'ArrowRight') {
carousel.scrollNext();
}
});
carousel.subscribe(function () {
const state = carousel.getSnapshot();
status.textContent =
`Slide ${state.selectedIndex + 1} of ${state.snapCount}`;
});
</script>
Updating a Live Carousel
update() changes configuration on an existing Vanilla JS instance. A pause button can toggle the autoplay option while keeping the current carousel state.
const pauseButton = document.querySelector('.carousel-pause');
let autoplayEnabled = true;
pauseButton.addEventListener('click', function () {
autoplayEnabled = !autoplayEnabled;
carousel.update({
autoplay: autoplayEnabled
});
pauseButton.textContent = autoplayEnabled ? 'Pause' : 'Play';
});
Framework Adapters
Plain JavaScript projects only need @midstem/swipi. Framework-specific packages wrap the same engine in APIs suited to each framework.
@midstem/swipi-react@midstem/swipi-vue@midstem/swipi-svelte@midstem/swipi-angular
Alternatives
- Embla Carousel: Draggable & Touch-friendly Carousel In Vanilla JavaScript
- Create Modern Mobile-friendly Carousels With Slickless.js
- Touch-Ready, High-Performance Vanilla JS Slider – Pagiflow
- Native Scroll Carousel Web Component with Drag – Blossom Carousel







