CSS-first slider for the modern web
bun add aero-slider Interact with the slider to see events
Examples
Multi-Slide View
Show multiple slides at once with gap spacing.
Viewport Breakout
Let slides overflow past the content column while the scroll track spans the full viewport. The active card's left edge stays locked to the normal content column, and the neighboring slides bleed off toward the screen edges.
Set the outer wrapper to full bleed, add the library's
aero-slider--breakout
class, and tie slide sizing to the content column with
--aero-layout-width.
The library derives the edge gutter automatically, so the first and last cards land on the
column. This demo uses
alignment: "left"
for the Apple-style left-flush layout.
:root {
--page-gutter: 1.5rem;
}
body {
overflow-x: hidden;
}
/* Full-bleed wrapper lets slides reach the screen edges */
.breakout-clip {
width: 100vw;
margin-left: calc(50% - 50vw);
}
/* Size slides off the content column; the library aligns the active
card to that column and derives the edge gutter automatically. */
.my-breakout {
--slides-per-view: 1.35;
--slide-gap: 18px;
--aero-layout-width: min(72rem, calc(100vw - var(--page-gutter) * 2));
}
/* createSlider(el, { alignment: "left" }); */ Alignment
Use alignment
to snap the active slide to the left, center, or right side of the viewport. Center is
the default.
Left
Center
Right
createSlider(leftEl, { alignment: "left", loop: true });
createSlider(centerEl, { alignment: "center", loop: true });
createSlider(rightEl, { alignment: "right", loop: true }); Responsive with Media Queries
Layout adapts to viewport: 1 slide on mobile, 2 on tablet, 3 on desktop. Resize the
window to see the slider re-read
--slides-per-view
from CSS.
/* 1 slide on mobile, 2 on tablet, 3 on desktop */
.aero-slider--responsive {
--slides-per-view: 1;
--slide-gap: 8px;
}
@media (min-width: 640px) {
.aero-slider--responsive {
--slides-per-view: 2;
--slide-gap: 12px;
}
}
@media (min-width: 1024px) {
.aero-slider--responsive {
--slides-per-view: 3;
--slide-gap: 16px;
}
} Autoplay + Loop
Auto-advances every 3 seconds with infinite looping. The countdown restarts after any manual change. Pauses on hover, keyboard focus, press, drag, and hidden tabs, and stops whenever prefers-reduced-motion is on.
Thumbnail Sync
A main slider plus a thumbnail strip: clicking a thumbnail navigates the main slider,
and when you change slides (drag, arrows, etc.), the thumbnail strip scrolls to keep the
active thumb in view. Use the
syncThumbnails()
function — both sliders must have the same number of slides. If you want the active
thumb centered, set
alignment: "center"
on the thumbnail slider.
How it works
Create two sliders: the main (primary) slider and a thumbnail slider with the same
number of slides. Call
syncThumbnails(primary, thumbnail)
to wire them up. The function adds click handlers to thumbnails, listens to
slideChange events
on the primary, scrolls the thumbnail strip to keep the active one in view, and adds
aero-slider__thumb--active
to the active thumbnail. It returns a teardown function — call it before destroying the
sliders.
HTML — main slider
<div id="main-slider" class="aero-slider">
<div class="aero-slider__viewport relative">
<button type="button" class="aero-slider__nav--prev" aria-label="Previous">…</button>
<button type="button" class="aero-slider__nav--next" aria-label="Next">…</button>
<div class="aero-slider__track">
<div>Slide 1</div>
<div>Slide 2</div>
<div>Slide 3</div>
</div>
</div>
</div> HTML — thumbnail slider
<div id="thumb-slider" class="aero-slider" style="--slides-per-view: 3; --slide-gap: 8px;">
<div class="aero-slider__viewport">
<div class="aero-slider__track">
<div>Thumb 1</div>
<div>Thumb 2</div>
<div>Thumb 3</div>
</div>
</div>
</div> JavaScript
import { createSlider, syncThumbnails } from "aero-slider";
const main = createSlider(document.getElementById("main-slider"));
const thumbs = createSlider(document.getElementById("thumb-slider"), {
alignment: "center",
});
const teardown = syncThumbnails(main, thumbs);
// On unmount:
// teardown();
// main.destroy();
// thumbs.destroy(); Direction
Set direction
to "rtl" for
right-to-left or
"ttb" for
vertical (top-to-bottom). Keyboard arrows, drag axis, and scroll axis all adapt
automatically.
RTL
Vertical (ttb)
// RTL
createSlider(rtlEl, { direction: "rtl" });
// Vertical
createSlider(verticalEl, { direction: "ttb" }); Dynamic Slides
Use add() and
remove() to
dynamically insert or remove slides. The slider rebuilds pagination, navigation, and
loop clones automatically.
const slider = createSlider(el);
// Add a new slide at the end
const slide = document.createElement("div");
slide.textContent = "New";
slider.add(slide);
// Add at a specific position
slider.add(slide, 2);
// Remove the last slide
slider.remove(slider.slideCount - 1);
// Remove multiple slides
slider.remove([0, 1]); Per-Move
Set perMove
to control how many slides advance per arrow/keyboard press. Here, 3 slides are visible
but each press moves by 2, with looping enabled.
createSlider(el, { perMove: 2, loop: true });
// With CSS: --slides-per-view: 3; --slide-gap: 12px; API Reference
createSlider(element, config?)
Creates a new slider instance on the given container element. Pagination and navigation are configured via DOM structure, not JavaScript—see the Pagination & Navigation example for required HTML.
import { createSlider } from "aero-slider";
import "aero-slider/slider.css";
const slider = createSlider(document.getElementById("my-slider"), {
loop: true,
draggable: true,
}); Without a bundler: Use the global script — see window.AeroSlider.
window.AeroSlider (global script)
When using the standalone script (no bundler), the library attaches an
AeroSlider
object to window
with two methods: createSlider and
syncThumbnails.
Include the CSS and JS files, then call AeroSlider.createSlider() and optionally
AeroSlider.syncThumbnails().
Use aero-slider.min.js with
type="module"
from unpkg
or the releases page.
<link rel="stylesheet" href="https://unpkg.com/aero-slider/dist/aero-slider.min.css" />
<script type="module" src="https://unpkg.com/aero-slider/dist/aero-slider.min.js"></script>
<script type="module">
// AeroSlider is on window after the module loads
const slider = AeroSlider.createSlider(document.getElementById("my-slider"), { loop: true });
</script> With thumbnails:
const main = AeroSlider.createSlider(mainEl);
const thumbs = AeroSlider.createSlider(thumbsEl);
const teardown = AeroSlider.syncThumbnails(main, thumbs);
// On unmount: teardown(); main.destroy(); thumbs.destroy(); | Property | Description |
|---|---|
| AeroSlider.createSlider | Same as the module createSlider — creates a slider instance |
| AeroSlider.syncThumbnails | Same as the module syncThumbnails — syncs primary and thumbnail sliders |
CSS Custom Properties
Layout is controlled via CSS custom properties on the container.
| Property | Default | Description |
|---|---|---|
| --slides-per-view | 1 | Number of visible slides (supports fractional, e.g. 1.5) |
| --slide-gap | 0px | Gap between slides |
| --slide-aspect | 16 / 9 | Aspect ratio for slides |
| --aero-layout-width | 100% | Content column width for aero-slider--breakout sliders. Slides size to this column while the track spans the full width; edge gutters are derived automatically. |
| data-aero-defer-visibility | — | Prevents layout shift before JS init in loop mode by reserving space for prepended clones. Pair it with data-aero-alignment in HTML when mirroring a non-default pre-init alignment; center is assumed otherwise. |
State Classes
The slider toggles these classes on the container, so you can style each state.
| Class | Present when |
|---|---|
| aero-slider--ready | The slider has initialized |
| aero-slider--draggable | draggable is on. The track shows the grab cursor only then |
| aero-slider--dragging | A mouse or pen drag is in progress (grabbing cursor) |
| aero-slider--vertical | direction is "ttb" |
SliderConfig
All configuration options (all optional with sensible defaults).
| Option | Type | Default | Description |
|---|---|---|---|
| loop | boolean | false | Infinite looping |
| alignment | "left" | "center" | "right" | "center" | Align the active slide to the left, center, or right when possible. At non-loop edges the scroll position clamps, but currentIndex still reflects the focused slide. |
| draggable | boolean | true | Enable mouse and pen dragging. Touch always swipes natively |
| autoplay | boolean | false | Auto-advance slides |
| autoplayInterval | number | 5000 | Delay between auto-advances (ms). Restarts after any manual change, and holds while the slider is pressed |
| maxDots | number | 0 | Max pagination dots (0 = unlimited); shows iOS-style sliding window when exceeded |
| noDrag | string | "" | CSS selector for elements inside slides that should not trigger drag (e.g. "a, button, input") |
| perMove | number | 1 | Number of slides to advance per next() / prev() call |
| direction | "ltr" | "rtl" | "ttb" | "ltr" | Carousel direction: left-to-right, right-to-left, or top-to-bottom (vertical) |
| scrollDuration | number | 0 | Length of next() / prev() / goTo() scrolls in ms, eased by scrollEasing, so they move the same way in every browser. 0 uses the browser's native smooth scrolling. Reduced motion makes every scroll instant either way |
| scrollEasing | (t: number) => number | ease-out cubic | Maps linear progress 0–1 to eased progress for scrollDuration scrolls, e.g. (t) => t for linear |
SliderInstance
Returned by createSlider().
The instance is also attached to the container element. You can access it via container.aeroSlider when you have a reference to the DOM element.
Method / Property Description .next() Go to next slide .prev() Go to previous slide .goTo(index, options?) Scroll to a specific slide index. Pass { instant: true } to jump there with no animation .pause() Hold autoplay until .resume(). Hover, focus and drag pauses still apply on top .resume() Release a .pause() and restart the autoplay countdown .update(config?) Update config and re-render .destroy() Tear down and clean up .refresh() Re-read slides from DOM and rebuild (call after external DOM changes) .add(slides, index?) Insert one or more slides into the track; calls refresh() automatically .remove(index) Remove slide(s) by index (number or array); calls refresh() automatically .element The container element the slider is attached to (read-only) .currentIndex Current active slide index (read-only) .slideCount Total number of slides (read-only)
For syncing a main slider with a thumbnail strip, use the standalone
syncThumbnails(primary, thumbnail)
function (see below).
syncThumbnails(primary, thumbnail, options?)
Syncs a primary slider with a thumbnail slider. Clicking a thumbnail navigates the
primary to that slide; when the primary changes (drag, arrows, etc.), the thumbnail
slider scrolls to keep the active thumbnail in view and adds
aero-slider__thumb--active
to it. The function does not force thumbnail alignment; set
alignment: "center"
on the thumbnail slider if you want the active thumb centered when possible. Both
sliders must have the same number of slides. Pass an optional third argument
{ loop }
to control whether the thumbnail strip loops (defaults to
true). Returns a
teardown function — call it before destroying the sliders.
import { createSlider, syncThumbnails } from "aero-slider";
const main = createSlider(mainEl);
const thumbs = createSlider(thumbsEl, {
alignment: "center",
});
const teardown = syncThumbnails(main, thumbs);
// On unmount:
// teardown();
// main.destroy();
// thumbs.destroy();
Events
Aero Slider dispatches native DOM events on the container element, prefixed with aero:.
Event data is available via event.detail.
Listen with addEventListener
on the container. Access the slider instance via event.currentTarget.aeroSlider in any handler.
const el = document.getElementById("my-slider");
const slider = createSlider(el, { loop: true });
el.addEventListener("aero:slideChange", (event) => {
console.log("active slide:", event.detail.index);
});
Event Data (event.detail, TS type) Description aero:ready {} Fired once after initialization (listen before createSlider) aero:slideChange { index: number } Fired when the active slide changes aero:dragStart { index: number } Fired when a mouse or pen drag starts (touch scrolls natively and doesn't fire drag events) aero:dragEnd { index: number; fromIndex: number } Fired when a mouse or pen drag is released, after the target slide is chosen aero:autoplayStart {} Fired when autoplay starts or resumes aero:autoplayStop {} Fired when autoplay pauses aero:visible { index: number } Fired per slide when it enters the visible range aero:hidden { index: number } Fired per slide when it leaves the visible range aero:resize {} Fired when the track or slide size changes, e.g. a window resize or a CSS variable such as --slides-per-view changing (before re-measuring) aero:resized {} Fired after the slider finishes updating from a resize aero:destroy {} Fired at the start of destroy(), before teardown
Browser Support
Aero Slider relies on modern CSS features to achieve a lightweight, JS-free layout. It degrades gracefully on older browsers, but for the optimal experience, the following minimum versions are recommended:
| CSS Feature | Chrome | Safari | Firefox | Used For |
|---|---|---|---|---|
| CSS Variables | 49+ | 9.1+ | 31+ | Configuring layout and gap size. |
| Scroll Snapping | 69+ | 11+ | 68+ | Locking slides into place after scrolling. |
| aspect-ratio | 88+ | 15+ | 89+ | Maintaining proper height for the slider track. |
| contain: paint | 52+ | 15.4+ | 69+ | Performance optimization for painting slides. |
| overscroll-behavior-x | 63+ | 16+ | 59+ | Preventing rubber-banding when hitting the edge. |