We are in the process of creating variants for v2 of the Carousel. There are no variants currently. In the meantime, use the Canvas to generate your own with AI in seconds.
Features
- Flexible item sizing with automatic recalculations across breakpoints
- Simple setup: navigation buttons and keyboard controls work with minimal markup
- Allows for multiple carousels per page without initialization conflicts
- Native CSS gives performant, GPU-accelerated scrolling
- Loop mode for infinite-style navigation
- Timed autoplay with configurable duration and smart pause/resume behavior
- Javascript API and events available for complex builds
Setup
How It Works
I started coding a carousel library on top of SwiperJS for days until I remembered how difficult it always is to work with. So I threw that library away. This new one was made to be super easy to work with (particularly on Webflow). You style elements, control item sizing, and manage spacing like you're always used to. No unexpected Javascript shenanigans.
In the background, ResizeObserver detects changes and automatically recalculates positions and boundaries. CSS Scroll Snap handles alignment. scrollIntoView() provides the smooth programmatic navigation.
The benefit of leveraging native browser features instead of a library like SwiperJS is that the carousel gets GPU acceleration and performance optimizations for free. And it's just smoother. Less JavaScript, better performance, smoother scrolling.
Structure
There's three elements required:
[data-carousel-container]– the outermost container, used for scoping a specific instance.[data-carousel-track]– the horizontal list that houses all the items.[data-carousel-item]– the individual items.
The purpose of the [data-carousel-container] element is three-fold:
- It allows for automatic unique instance detection. Translation: you can simply have elements like the previous/next navigation buttons exist within the
[data-carousel-container], and the code already knows those buttons are for that specific carousel. It won't randomly start moving another carousel on another part of the page. - It works perfectly with Webflow CMS. How do you get navigation buttons inside Webflow's Collection List if you can't put non-CMS things inside a Collection List? Exactly. Now you can.
You can put whatever you want inside of the [data-carousel-container] element. The [data-carousel-track] can be nested arbitrarily deep.
The only restraints: there can only be one carousel within a container, and all the [data-carousel-item] elements must be direct children of the track.
Webflow CMS
To match up with the Webflow Collection List structure:
[data-carousel-container]– either goes on theCollection List Wrapper(if you don't care for navigation buttons), or anywhere as an ancestor of theCollection List Wrapper.[data-carousel-track]– goes on theCollection List– goes on the (you guessed it!)
<div data-carousel-container>
<div data-carousel="track">
<div data-carousel="item">Item 1</div>
<div data-carousel="item">Item 2</div
Customization
Core Attributes
Configure the carousel with data attributes on the container element:
| Attribute | Values | Default | Description |
|---|---|---|---|
data-carousel-container | presence (skip with "false") | - | Required on container element |
data-carousel-track | presence (skip with "false") | - | Required on track element |
data-carousel-item | presence (skip with "false") | - | Required on each item element |
|
Loop
Enable infinite-style navigation so the carousel wraps around when reaching either end. When loop is active, the previous/next buttons are never disabled.
<div data-carousel-container data-carousel-loop>
<!-- ... -->
</div>Loop applies to all forms of navigation: buttons, keyboard, scroll markers, autoplay, and the JavaScript API.
Scroll-by
By default, the carousel advances one item at a time. Set data-carousel-scroll-by="page" to advance by the number of items currently visible in the viewport. This is useful for multi-item carousels where you want to scroll a full "page" of content at once.
<div data-carousel-container data-carousel-scroll-by="page">
<!-- ... -->
</div>The page calculation uses the actual container width, so it works correctly with variable-width items and across breakpoints.
Navigation Elements
Optional navigation controls that work automatically when placed inside the container:
| Attribute | Description |
|---|---|
data-carousel-prev | Previous button |
data-carousel-next | Next button |
data-carousel-restart | Restart button (go to first item + play) |
You can place multiple instances of any navigation button or counter element within the same container. All of them will receive click listeners, disabled states, and content updates. This is useful for placing prev/next buttons in multiple locations (e.g., above and below the track).
Scroll Markers
Add scroll markers anywhere inside the carousel container. The library finds all [data-carousel-marker] elements and clones the first one to match the number of navigable positions.
When items are narrower than the container (multi-item carousels), the last few items may share the same scroll position due to the browser's scroll ceiling. The library detects this and groups them into a single "snap position." Marker count reflects navigable positions, not raw item count. A console warning is logged when loop or autoplay is enabled with unreachable items. To make every item individually reachable, use wider items or add padding-inline-end to the track.
<div data-carousel-container>
<div data-carousel="track">
<div data-carousel="item">...</div>
<div data-carousel="item">...</div
| Attribute | Description |
|---|---|
data-carousel-marker | Individual marker element (first one is used as template) |
data-carousel-counter-current | Element that displays current item number (1-based) |
data-carousel-counter-total | Element that displays total navigable positions |
For custom counter displays like "2 of 5":
<div class="my-marker-styles">
<button data-carousel-marker></button>
</div>
<div>
<span data-carousel-counter-current></span>
of
<span data-carousel-counter-total></
The library automatically adds aria-label="Scroll to item X of Y" and aria-selected="true" on the active marker for accessibility.
Scroll markers use roving tabindex for keyboard navigation. Only the active marker is in the tab order — pressing Tab skips past inactive markers. When a marker has focus, Arrow Left / Arrow Right move between markers (wrapping when data-carousel-loop is enabled), and Home / End jump to the first and last marker.
Keyboard Navigation
When enabled, Arrow Left and Arrow Right navigate between items. For those with huge keyboards, Home jumps to first item and End jumps to the last item.
To enable keyboard navigation:
<div data-carousel-container data-carousel-keyboard>
<!-- Rest of the track/items -->
</div>Autoplay
Automatically advance items on a timer. Combine with data-carousel-loop for continuous cycling. Without loop, autoplay advances through all items, completes the progress animation on the last item, then stops cleanly — the container will have both .carousel-at-end (position) and not .carousel-playing (stopped).
<div data-carousel-container data-carousel-loop data-carousel-autoplay>
<!-- ... -->
</div>Autoplay Attributes
| Attribute | Values | Default | Description |
|---|---|---|---|
data-carousel-autoplay | presence (skip with "false") | - | Enable timed autoplay |
data-carousel-autoplay-duration | number (ms) | 5000 | Time per item in milliseconds |
data-carousel-autoplay-pause-hover | presence (skip with "false") | - | Temporarily pause autoplay on track hover |
|
Autoplay with Custom Duration
<div
data-carousel-container
data-carousel-loop
data-carousel-autoplay
data-carousel-autoplay-duration="3000"
>
<!-- Advances every 3 seconds -->
</div>Play/Pause Button
Add a toggle button inside the container to let users control autoplay. The library toggles aria-label between "Stop autoplay" and "Start autoplay" automatically.
<div data-carousel-container data-carousel-loop data-carousel-autoplay>
<div data-carousel="track">
<div data-carousel="item">...</div>
<div data-carousel="item">...
Restart Button
Add a restart button to let users go back to the first item and start autoplay again. Useful for non-loop carousels where autoplay has completed.
<div data-carousel-container data-carousel-autoplay>
<div data-carousel="track">
<div data-carousel="item">...</div>
<div data-carousel="item">...</
Clicking the restart button navigates to the first item and starts autoplay fresh. Since navigating to the first item updates position classes, .carousel-at-end is removed and .carousel-at-start is added.
Pause Behavior
Autoplay uses a stop-on-action model. Any intentional user interaction fully stops autoplay. Only the play button (or calling play()) restarts it.
Actions that stop autoplay:
- Navigation buttons (prev/next): Clicking prev/next stops autoplay.
- Marker clicks: Clicking a marker stops autoplay.
- Keyboard navigation: Arrow keys, Home, and End stop autoplay.
- Dragging/swiping: Manually scrolling the track to a different item stops autoplay.
- JavaScript API: Calling
next(),prev(),goTo(), orstop()stops autoplay.
Temporary pauses (autoplay resumes automatically):
- Hover: Mouse enters the track (resumes on mouse leave). Opt-in via
data-carousel-autoplay-pause-hover="true". Only the track area triggers this — hovering nav buttons or markers outside the track does not pause. - Focus: A focusable element inside the track receives focus (resumes when focus leaves the track). Configurable via
data-carousel-autoplay-pause-focus. Focus moving to a nav button outside the track will resume autoplay. - Viewport: The carousel scrolls out of view (resumes when at least 50% is visible again). Uses
IntersectionObserver.
Reduced Motion
When the user's operating system has prefers-reduced-motion: reduce enabled, autoplay will not start. The container receives the carousel-reduced-motion class. Calling play() via JavaScript is also blocked.
Spacing and Positioning
Use CSS gap on the [data-carousel-track] to control spacing.
The library also works with CSS scroll-padding (container insets) and scroll-margin (per-item offsets) for controlling snap positioning. For the variants on this page, all of them have scroll-padding on the track element.
CSS Custom Properties
The library exposes state as CSS custom properties on the container element. Use these for dynamic styling without JavaScript.
| Property | Values | Set On | Description |
|---|---|---|---|
--carousel-index | 1, 2, 3… | Container | Current item number (1-based) |
--carousel-total | 1, 2, 3… | Container | Total navigable positions (may be fewer than item count in multi-item carousels) |
--carousel-progress | 0 – |
Example: a progress bar that fills as you scroll through the carousel.
.carousel-progress-bar {
width: calc(var(--carousel-progress) * 100%);
height: 2px;
background: currentColor;
transition: width 150ms ease-out
Example: an autoplay progress indicator on each marker. Since --carousel-autoplay-progress is set on the active marker (and reset to 0 on inactive markers), you can use it directly to build a per-item timer.
.carousel-marker-progress {
transform: scaleX(var(--carousel-autoplay-progress, 0));
transform-origin: left;
transition: none;
}State Classes
The library applies state classes that you can style however you want.
| Class | Applied to |
|---|---|
.carousel-item-active | The item that is currently active (i.e. aligned) |
.carousel-nav-disabled | Navigation buttons at start/end boundaries (never applied when looping) |
.carousel-scrolling | The track while scrolling is active |
.carousel-marker-active | The active scroll marker |
.carousel-playing | The container while autoplay is actively running |
.carousel-at-start | The container when at the first navigable position (never applied when looping) |
Example: style the play/pause button based on autoplay state.
/* Show pause icon while playing, play icon when stopped */
.carousel-playing [data-carousel-play-pause] .play-icon {
display: none;
}
.carousel-playing [data-carousel-play-pause] .pause-icon {
display: block;
}JavaScript API
Manual Initialization
// Auto-initializes on page load by default
// Or manually initialize:
const carousel = new Carousel(
document.querySelector('[data-carousel-container]')
);
// Or using selector:
const carousel = CarouselInstance Methods
carousel.next(); // Go to next item (stops autoplay)
carousel.prev(); // Go to previous item (stops autoplay)
carousel.goTo(2); // Go to specific index (stops autoplay)
carousel
All methods are chainable (except getActiveIndex and destroy):
carousel.next().next().refresh();play()
Starts autoplay fresh with a full duration timer. Has no effect when prefers-reduced-motion: reduce is active. Requires data-carousel-autoplay on the container — logs a warning if called without it.
stop()
Stops autoplay completely. The carousel-playing class is removed and progress resets to 0. Only play() or clicking the play/pause button will restart it.
goTo(index)
Navigates to the given 0-based index. If the index is beyond the last navigable position (in multi-item carousels where the last few items share a scroll position), it is silently clamped. If autoplay is running, it is stopped.
Events
The carousel dispatches DOM CustomEvents on the container element. Listen for them with addEventListener:
const container = document.querySelector('[data-carousel-container]');
container.addEventListener('carousel:snapchange', (e
Available events:
| Event | Description | e.detail |
|---|---|---|
carousel:snapchange | Active item changed | { index } |
carousel:scroll | Track scrolled | { scrollLeft } |
carousel:reach-start | Scrolled to first item | - |
|
carousel:reach-start and carousel:reach-end fire based on physical scroll position, even when loop mode is enabled. They reflect the actual scroll edges, not the logical navigation boundaries.
Instance Access
Access a carousel instance through the DOM element:
const container = document.querySelector('[data-carousel-container]');
const carousel = container._carousel;
carousel.next().next().refresh()Dynamic Content
When adding/removing items, call refresh():
const track = carousel.track;
const newItem = document.createElement('div');
newItem.setAttribute('data-carousel-item', '');
newItem.
For major structural changes, destroy and reinitialize:
carousel.destroy();
const newCarousel = new Carousel(container);Accessibility
The library adds semantic ARIA attributes automatically. You don't need to add any of these yourself — they're listed here so you know what to expect in the DOM.
| Feature | What the library does |
|---|---|
| Track & items | Sets role="list" on the track and role="listitem" on each item |
| Markers | Sets role="tablist" on the marker group and role="tab" on each marker. The active marker gets aria-selected="true" |
| Live region | Injects a visually hidden <div> with that announces "Item X of Y" on slide changes |
All role attributes respect existing values — if you set a role on the track or markers in your HTML, the library won't override it.