Carousel
Touch-enabled content sliders built on a real scroll container with CSS scroll-snap, keyboard navigation, and autoplay controls.
Introduction
Chassis CSS carousels are content sliders built on .carousel-inner, a real horizontally-scrolling container using CSS scroll-snap, rather than JavaScript-driven position transforms. The browser handles touch, trackpad, and mouse-drag scrolling natively; the Carousel plugin layers on keyboard navigation, autoplay, the prev/next and indicator controls, and end-of-track behavior.
Carousels require explicit JavaScript initialization — construct an instance with the Carousel class before user interaction. A carousel decorated with data-cx-autoplay="true" initializes automatically on page load; constructing it again manually creates a second instance without disposing the first, leaving its listeners and IntersectionObserver active, so pick one initialization path per carousel.
Nested carousels are not supported: prev/next controls and indicators are located by scanning the entire carousel subtree, so an inner carousel's controls would also be picked up by the outer instance. Provide an alternative navigation pattern for carousel-like content nested inside another carousel.
Chassis CSS respects user accessibility preferences by automatically disabling animations when the prefers-reduced-motion media query is detected. See the reduced motion guidelines in our accessibility documentation for implementation details.
Basic structure
This foundational example shows the essential carousel structure: navigation controls, indicators, and slides. Controls work as semantic <button> elements; <a> elements are also supported as controls but need role="button", since they carry no href.
<div id="carouselExample" class="carousel slide">
<div class="d-flex justify-content-between align-items-center">
<div>
<button class="button sm icon-only" type="button" data-cx-target="#carouselExample" data-cx-slide="prev">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-left-solid"></use></svg>
<span class="visually-hidden">Previous</span>
</button>
<button class="button sm icon-only" type="button" data-cx-target="#carouselExample" data-cx-slide="next">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-right-solid"></use></svg>
<span class="visually-hidden">Next</span>
</button>
</div>
<ol class="carousel-indicators">
<li><button type="button" data-cx-target="#carouselExample" data-cx-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button></li>
<li><button type="button" data-cx-target="#carouselExample" data-cx-slide-to="1" aria-label="Slide 2"></button></li>
<li><button type="button" data-cx-target="#carouselExample" data-cx-slide-to="2" aria-label="Slide 3"></button></li>
</ol>
</div>
<div class="carousel-inner">
<div class="carousel-item active">
<img src="..." class="d-block w-100" alt="...">
</div>
<div class="carousel-item">
<img src="..." class="d-block w-100" alt="...">
</div>
<div class="carousel-item">
<img src="..." class="d-block w-100" alt="...">
</div>
</div>
</div>Bottom controls
Place navigation controls below the carousel for a more traditional layout.
<div id="carouselBottomExample" class="carousel slide">
<div class="carousel-inner">
<div class="carousel-item active">
<img src="..." class="d-block w-100" alt="...">
</div>
<div class="carousel-item">
<img src="..." class="d-block w-100" alt="...">
</div>
<div class="carousel-item">
<img src="..." class="d-block w-100" alt="...">
</div>
</div>
<div class="d-flex justify-content-between align-items-center">
<div>
<button class="button sm icon-only" type="button" data-cx-target="#carouselBottomExample" data-cx-slide="prev">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-left-solid"></use></svg>
<span class="visually-hidden">Previous</span>
</button>
<button class="button sm icon-only" type="button" data-cx-target="#carouselBottomExample" data-cx-slide="next">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-right-solid"></use></svg>
<span class="visually-hidden">Next</span>
</button>
</div>
<ol class="carousel-indicators">
<li><button type="button" data-cx-target="#carouselBottomExample" data-cx-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button></li>
<li><button type="button" data-cx-target="#carouselBottomExample" data-cx-slide-to="1" aria-label="Slide 2"></button></li>
<li><button type="button" data-cx-target="#carouselBottomExample" data-cx-slide-to="2" aria-label="Slide 3"></button></li>
</ol>
</div>
</div>Overlay controls
Wrap controls and indicators in a .carousel-overlay container to overlay them on the slides.
<div id="carouselOverlayExample" class="carousel" data-cx-autoplay="true">
<div class="carousel-inner">
<div class="carousel-item active">
<img src="..." class="d-block w-100" alt="...">
</div>
<div class="carousel-item">
<img src="..." class="d-block w-100" alt="...">
</div>
<div class="carousel-item">
<img src="..." class="d-block w-100" alt="...">
</div>
</div>
<div class="carousel-overlay">
<button class="button sm icon-only" type="button" data-cx-target="#carouselOverlayExample" data-cx-slide="prev">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-left-solid"></use></svg>
<span class="visually-hidden">Previous</span>
</button>
<ol class="carousel-indicators">
<li><button type="button" data-cx-target="#carouselOverlayExample" data-cx-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button></li>
<li><button type="button" data-cx-target="#carouselOverlayExample" data-cx-slide-to="1" aria-label="Slide 2"></button></li>
<li><button type="button" data-cx-target="#carouselOverlayExample" data-cx-slide-to="2" aria-label="Slide 3"></button></li>
</ol>
<button class="button sm icon-only" type="button" data-cx-target="#carouselOverlayExample" data-cx-slide="next">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-right-solid"></use></svg>
<span class="visually-hidden">Next</span>
</button>
</div>
</div>Fade transitions
Replace the scroll transition with a crossfade using .carousel-fade. Fade mode stacks every slide instead of scrolling between them, fading the active one in over --carousel-fade-duration; for optimal crossfading with certain content types, consider adding .bg-main or a custom background to .carousel-item.
<div id="carouselExampleFade" class="carousel slide carousel-fade">
<div class="carousel-inner">
<div class="carousel-item active">
<img src="..." class="d-block w-100" alt="...">
</div>
<div class="carousel-item">
<img src="..." class="d-block w-100" alt="...">
</div>
<div class="carousel-item">
<img src="..." class="d-block w-100" alt="...">
</div>
</div>
<div class="carousel-overlay">
<button class="button sm icon-only" type="button" data-cx-target="#carouselExampleFade" data-cx-slide="prev">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-left-solid"></use></svg>
<span class="visually-hidden">Previous</span>
</button>
<button class="button sm icon-only" type="button" data-cx-target="#carouselExampleFade" data-cx-slide="next">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-right-solid"></use></svg>
<span class="visually-hidden">Next</span>
</button>
</div>
</div>Autoplay
Set the autoplay option to true (or add data-cx-autoplay="true") to cycle a carousel automatically on page load. Autoplay pauses on mouseenter by default (the pause option) and stops advancing once the page or the carousel itself isn't visible, per the Page Visibility API. The first control click, arrow-key press, or track drag/tap turns autoplay off for good, so content doesn't keep moving out from under someone who's just started interacting with it — part of complying with WCAG 2.2 Success Criterion 2.2.2 Pause, Stop, Hide.
Chassis CSS recommends providing explicit pause/stop controls for autoplaying carousels to ensure compliance with WCAG 2.2 Success Criterion 2.2.2 Pause, Stop, Hide and improve user experience for all users.
Add a button with .carousel-control-play-pause class (shown below) to allow users to toggle autoplay on and off. The button automatically updates its icon and accessible name to reflect the current state.
The play/pause button toggles autoplay and reflects the current state automatically—a pause icon while playing, a play icon while stopped. Give each state an accessible name with the data-cx-play-label and data-cx-pause-label attributes. While playing, the active indicator fills like a progress bar over the current slide's interval as a visual countdown; the fill pauses with autoplay and is skipped entirely under reduced motion. Set its color with the --cx-carousel-indicator-active-color CSS variable.
<div id="carouselExampleAutoplaying" class="carousel slide" data-cx-autoplay="true">
<div class="carousel-inner">
<div class="carousel-item active">
<img src="..." class="d-block w-100" alt="...">
</div>
<div class="carousel-item">
<img src="..." class="d-block w-100" alt="...">
</div>
<div class="carousel-item">
<img src="..." class="d-block w-100" alt="...">
</div>
</div>
<div class="d-flex justify-content-between align-items-center">
<div>
<button class="carousel-control-play-pause button sm icon-only" type="button" data-cx-target="#carouselExampleAutoplaying" aria-label="Pause" data-cx-pause-label="Pause" data-cx-play-label="Play">
<svg class="icon carousel-icon-pause" aria-hidden="true"><use href="#pause-solid"></use></svg>
<svg class="icon carousel-icon-play" aria-hidden="true"><use href="#play-solid"></use></svg>
</button>
<button class="button sm icon-only" type="button" data-cx-target="#carouselExampleAutoplaying" data-cx-slide="prev">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-left-solid"></use></svg>
<span class="visually-hidden">Previous</span>
</button>
<button class="button sm icon-only" type="button" data-cx-target="#carouselExampleAutoplaying" data-cx-slide="next">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-right-solid"></use></svg>
<span class="visually-hidden">Next</span>
</button>
</div>
<ol class="carousel-indicators">
<li><button type="button" data-cx-target="#carouselExampleAutoplaying" data-cx-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button></li>
<li><button type="button" data-cx-target="#carouselExampleAutoplaying" data-cx-slide-to="1" aria-label="Slide 2"></button></li>
<li><button type="button" data-cx-target="#carouselExampleAutoplaying" data-cx-slide-to="2" aria-label="Slide 3"></button></li>
</ol>
</div>
</div>Timing control
Add data-cx-interval="" to a .carousel-item to change the amount of time to delay before autoplay advances past that item. The indicator's progress fill matches each slide's own interval, so the countdown speeds up or slows down to match (2s, then 4s, then 6s below).
<div id="carouselExampleInterval" class="carousel slide" data-cx-autoplay="true">
<div class="carousel-inner rounded-5">
<div class="carousel-item active" data-cx-interval="2000">
<img src="..." class="d-block w-100" alt="...">
</div>
<div class="carousel-item" data-cx-interval="4000">
<img src="..." class="d-block w-100" alt="...">
</div>
<div class="carousel-item" data-cx-interval="6000">
<img src="..." class="d-block w-100" alt="...">
</div>
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<button class="carousel-control-play-pause button sm icon-only" type="button" data-cx-target="#carouselExampleInterval" aria-label="Pause" data-cx-pause-label="Pause" data-cx-play-label="Play">
<svg class="icon carousel-icon-pause" aria-hidden="true"><use href="#pause-solid"></use></svg>
<svg class="icon carousel-icon-play" aria-hidden="true"><use href="#play-solid"></use></svg>
</button>
<ol class="carousel-indicators">
<li><button type="button" data-cx-target="#carouselExampleInterval" data-cx-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button></li>
<li><button type="button" data-cx-target="#carouselExampleInterval" data-cx-slide-to="1" aria-label="Slide 2"></button></li>
<li><button type="button" data-cx-target="#carouselExampleInterval" data-cx-slide-to="2" aria-label="Slide 3"></button></li>
</ol>
</div>
</div>End behavior
The ends option controls what happens at the first and last slide. Set it with data-cx-ends (or the ends option in JavaScript), choosing from loop (the default), wrap, or stop.
Loop (default)
With data-cx-ends="loop" the carousel scrolls seamlessly past the ends: stepping next from the last slide (or previous from the first) continues in the same direction into the destination slide instead of jumping back, for an endless conveyor effect. This is the default, so it applies with or without the attribute. Seamless looping applies to single-slide carousels driven by the controls, keyboard, or autoplay; multi-item, peek, centered, and variable-width layouts—as well as users who prefer reduced motion—fall back to the wrap jump.
<div id="carouselEndsLoop" class="carousel slide" data-cx-ends="loop">
<div class="carousel-inner">
<div class="carousel-item active">
<img src="..." class="d-block w-100" alt="...">
</div>
<div class="carousel-item">
<img src="..." class="d-block w-100" alt="...">
</div>
<div class="carousel-item">
<img src="..." class="d-block w-100" alt="...">
</div>
</div>
<div class="d-flex justify-content-center">
<button class="button sm icon-only" type="button" data-cx-target="#carouselEndsLoop" data-cx-slide="prev">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-left-solid"></use></svg>
<span class="visually-hidden">Previous</span>
</button>
<button class="button sm icon-only" type="button" data-cx-target="#carouselEndsLoop" data-cx-slide="next">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-right-solid"></use></svg>
<span class="visually-hidden">Next</span>
</button>
</div>
</div>Wrap
With data-cx-ends="wrap" the carousel jumps from the last slide back to the first, and vice versa, so the controls never reach a dead end.
<div id="carouselEndsWrap" class="carousel slide" data-cx-ends="wrap">
<div class="carousel-inner">
<div class="carousel-item active">
<img src="..." class="d-block w-100" alt="...">
</div>
<div class="carousel-item">
<img src="..." class="d-block w-100" alt="...">
</div>
<div class="carousel-item">
<img src="..." class="d-block w-100" alt="...">
</div>
</div>
<div class="d-flex justify-content-center">
<button class="button sm icon-only" type="button" data-cx-target="#carouselEndsWrap" data-cx-slide="prev">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-left-solid"></use></svg>
<span class="visually-hidden">Previous</span>
</button>
<button class="button sm icon-only" type="button" data-cx-target="#carouselEndsWrap" data-cx-slide="next">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-right-solid"></use></svg>
<span class="visually-hidden">Next</span>
</button>
</div>
</div>Stop
With data-cx-ends="stop" the carousel hard-stops at the first and last slide. The previous control is automatically disabled on the first slide and the next control on the last (via the native disabled attribute on <button> controls); when a focused control becomes disabled, focus shifts to the opposite control so it isn't lost.
<div id="carouselEndsStop" class="carousel slide" data-cx-ends="stop">
<div class="carousel-inner">
<div class="carousel-item active">
<img src="..." class="d-block w-100" alt="...">
</div>
<div class="carousel-item">
<img src="..." class="d-block w-100" alt="...">
</div>
<div class="carousel-item">
<img src="..." class="d-block w-100" alt="...">
</div>
</div>
<div class="d-flex justify-content-center">
<button class="button sm icon-only" type="button" data-cx-target="#carouselEndsStop" data-cx-slide="prev">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-left-solid"></use></svg>
<span class="visually-hidden">Previous</span>
</button>
<button class="button sm icon-only" type="button" data-cx-target="#carouselEndsStop" data-cx-slide="next">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-right-solid"></use></svg>
<span class="visually-hidden">Next</span>
</button>
</div>
</div>Content sizing
Any markup can go inside a .carousel-item, and CSS custom properties on .carousel control how many slides show at once and how they're sized—no extra JavaScript beyond the base plugin.
Custom content
Any markup can go inside a .carousel-item — add it, then size and style it with utilities or custom CSS.
<div id="carouselExampleContent" class="carousel slide">
<div class="carousel-inner rounded-xl">
<div class="carousel-item active">
<div class="d-flex flex-column justify-content-center primary-bg-even p-6xl" style="min-height: 320px;">
<h3>Build anything</h3>
<p>Compose slides from your own markup—text, buttons, cards, or media.</p>
<div><a class="button primary" href="#">Get started</a></div>
</div>
</div>
<div class="carousel-item">
<div class="d-flex flex-column justify-content-center text-center success-bg-even p-6xl" style="min-height: 320px;">
<h3>Style it your way</h3>
<p>Use utilities or custom CSS to size and theme each slide however you need.</p>
<div><a class="button success" href="#">Learn more</a></div>
</div>
</div>
<div class="carousel-item">
<div class="d-flex flex-column justify-content-center text-end warning-bg-even p-6xl" style="min-height: 320px;">
<h3>Mix and match</h3>
<p>Combine custom content with controls, indicators, and the overlay layout.</p>
<div><a class="button black" href="#">Browse examples</a></div>
</div>
</div>
</div>
<div class="d-flex justify-content-center">
<button class="button sm icon-only" type="button" data-cx-target="#carouselExampleContent" data-cx-slide="prev" style="--cx-carousel-control-icon-width: 1rem;">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-left-solid"></use></svg>
<span class="visually-hidden">Previous</span>
</button>
<button class="button sm icon-only" type="button" data-cx-target="#carouselExampleContent" data-cx-slide="next" style="--cx-carousel-control-icon-width: 1rem;">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-right-solid"></use></svg>
<span class="visually-hidden">Next</span>
</button>
</div>
</div>Multiple items
Set --cx-carousel-items inline to show several slides at once. Combine it with --cx-carousel-items-gap for spacing. Since seamless looping only applies to single-slide carousels (see Loop (default)), this example sets data-cx-ends="stop" for a clean boundary instead of a wrap jump.
<div id="carouselMultiple" class="carousel slide" data-cx-ends="stop"
style="--cx-carousel-items: 3; --cx-carousel-items-gap: 1rem;">
<div class="carousel-inner">
<div class="carousel-item active"><img src="..." class="d-block w-100 rounded" alt="..."></div>
<div class="carousel-item"><img src="..." class="d-block w-100 rounded" alt="..."></div>
<div class="carousel-item"><img src="..." class="d-block w-100 rounded" alt="..."></div>
<div class="carousel-item"><img src="..." class="d-block w-100 rounded" alt="..."></div>
<div class="carousel-item"><img src="..." class="d-block w-100 rounded" alt="..."></div>
<div class="carousel-item"><img src="..." class="d-block w-100 rounded" alt="..."></div>
<div class="carousel-item"><img src="..." class="d-block w-100 rounded" alt="..."></div>
</div>
<div class="d-flex justify-content-center">
<button class="button sm icon-only" type="button" data-cx-target="#carouselMultiple" data-cx-slide="prev">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-left-solid"></use></svg>
<span class="visually-hidden">Previous</span>
</button>
<button class="button sm icon-only" type="button" data-cx-target="#carouselMultiple" data-cx-slide="next">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-right-solid"></use></svg>
<span class="visually-hidden">Next</span>
</button>
</div>
</div>--cx-carousel-itemssets the number of whole slides visible per view (default1).--cx-carousel-items-gapsets the space between slides (default0).
Peeking
Set --cx-carousel-items-peek inline to reveal a sliver of the previous and next slides, hinting that there's more to scroll.
<div id="carouselPeek" class="carousel slide" data-cx-ends="stop"
style="--cx-carousel-items-peek: 3rem; --cx-carousel-items-gap: 1rem;">
<div class="carousel-inner">
<div class="carousel-item active"><img src="..." class="d-block w-100 rounded-xl" alt="..."></div>
<div class="carousel-item"><img src="..." class="d-block w-100 rounded-xl" alt="..."></div>
<div class="carousel-item"><img src="..." class="d-block w-100 rounded-xl" alt="..."></div>
<div class="carousel-item"><img src="..." class="d-block w-100 rounded-xl" alt="..."></div>
<div class="carousel-item"><img src="..." class="d-block w-100 rounded-xl" alt="..."></div>
</div>
<div class="d-flex justify-content-center">
<button class="button sm icon-only" type="button" data-cx-target="#carouselPeek" data-cx-slide="prev">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-left-solid"></use></svg>
<span class="visually-hidden">Previous</span>
</button>
<button class="button sm icon-only" type="button" data-cx-target="#carouselPeek" data-cx-slide="next">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-right-solid"></use></svg>
<span class="visually-hidden">Next</span>
</button>
</div>
</div>--cx-carousel-items-peeksets how much of the neighboring slides to reveal (default0).
Variable width
Add .carousel-auto and size each .carousel-item individually with utilities or custom CSS; snap points still land on every slide.
<div id="carouselVariable" class="carousel carousel-auto slide" data-cx-ends="stop" style="--cx-carousel-items-gap: 1rem;">
<div class="carousel-inner">
<div class="carousel-item active" style="width: 160px;">
<img src="..." class="d-block w-100 rounded" alt="...">
</div>
<div class="carousel-item" style="width: 320px;">
<img src="..." class="d-block w-100 rounded" alt="...">
</div>
<div class="carousel-item" style="width: 220px;">
<img src="..." class="d-block w-100 rounded" alt="...">
</div>
<div class="carousel-item" style="width: 380px;">
<img src="..." class="d-block w-100 rounded" alt="...">
</div>
<div class="carousel-item" style="width: 200px;">
<img src="..." class="d-block w-100 rounded" alt="...">
</div>
<div class="carousel-item" style="width: 300px;">
<img src="..." class="d-block w-100 rounded" alt="...">
</div>
</div>
<div class="d-flex justify-content-center">
<button class="button sm icon-only" type="button" data-cx-target="#carouselVariable" data-cx-slide="prev">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-left-solid"></use></svg>
<span class="visually-hidden">Previous</span>
</button>
<button class="button sm icon-only" type="button" data-cx-target="#carouselVariable" data-cx-slide="next">
<svg class="icon directional-icon" aria-hidden="true"><use href="#chevron-right-solid"></use></svg>
<span class="visually-hidden">Next</span>
</button>
</div>
</div>Add .carousel-center to snap the active slide to the center of the viewport instead of its start — pairs well with --cx-carousel-items-peek.
Accessibility
Carousel accessibility centers on keyboard control, focus preservation at the ends, and ARIA state that mirrors the current slide.
Keyboard navigation
With the keyboard option enabled (the default), Left Arrow and Right Arrow move to the previous and next slide while focus is inside the carousel; either key turns autoplay off for good, the same as a control click. Arrow keys inside a nested <input>, <textarea>, <select>, or [contenteditable] element are left alone so native form navigation keeps working.
Focus at the ends
With ends: "stop", a control at a disabled end can't hold focus. Before disabling it, the plugin moves focus to the opposite control if that one stays enabled, or to the carousel viewport itself — making it focusable with tabindex="-1" if it isn't already — so focus never lands on nothing.
ARIA state
The plugin sets aria-current="true" on the active indicator and removes it from the previous one; give every indicator button its own aria-label identifying the slide it jumps to (aria-label="Slide 1"). Controls implemented as <a> rather than <button> have no native disabled state, so a disabled end sets aria-disabled="true" instead — pair <a> controls with role="button" (see Basic structure).
JavaScript API
The Carousel plugin manages slide navigation, autoplay, and end-of-track control state. Chassis JS ships as an ES module — import the Carousel class:
import { Carousel } from '@chassis-ui/css'Triggers
The data-attribute API drives navigation and autoplay without writing any custom JavaScript:
<button data-cx-target="#myCarousel" data-cx-slide="prev">Previous</button>
<button data-cx-target="#myCarousel" data-cx-slide="next">Next</button>
<button data-cx-target="#myCarousel" data-cx-slide-to="2">Slide 3</button>
<div id="myCarousel" class="carousel slide" data-cx-autoplay="true" data-cx-ends="stop">
...
</div>| Attribute | Values | Description |
|---|---|---|
data-cx-target | CSS selector | The .carousel element a control acts on. |
data-cx-slide | prev, next | Moves relative to the current slide. |
data-cx-slide-to | number | Zero-based index of the slide to show. |
data-cx-autoplay | true | Autoplays on load. Also marks the element for automatic initialization. |
data-cx-interval | number (ms) | On the carousel root, overrides the interval option. On a .carousel-item, overrides the wait before autoplay advances past that slide. |
data-cx-ends | loop, wrap, stop | Overrides the ends option. |
data-cx-keyboard | true, false | Overrides the keyboard option. |
data-cx-pause | hover, false | Overrides the pause option. |
data-cx-play-label | string | On .carousel-control-play-pause, the aria-label to apply while autoplay is stopped. |
data-cx-pause-label | string | On .carousel-control-play-pause, the aria-label to apply while autoplay is playing. |
Initialization
For programmatic access — calling methods or listening to events — instantiate each element with the Carousel class:
const carousel = new Carousel('#myCarousel')
// With options
const carousel = new Carousel('#myCarousel', {
interval: 2000,
ends: 'stop'
})Options
Options are set via data-cx-* attributes or passed to the constructor as an object. Attribute names use the kebab-case form of the option name — data-cx-custom-class, not data-cx-customClass. Attribute values are parsed to their native types:"true" → true, "0" → 0, and valid JSON strings to objects.
Use data-cx-config to pass multiple options as a JSON string:data-cx-config='{"delay":200}'. Individual data-cx-* attributes take precedence over data-cx-config. You can also use JSON values in individual attributes, such as data-cx-delay='{"show":100,"hide":200}'.
When initializing components, Chassis merges configurations from multiple sources in this priority order: default settings, data-cx-config values, individual data-cx-*attributes, and finally any JavaScript object options. Values defined later in this sequence override earlier ones.
| Option | Type | Default | Description |
|---|---|---|---|
autoplay | boolean | false | Autoplays the carousel on load. |
ends | string | "loop" | Behavior at the first/last slide. "stop" hard-stops and disables the previous control on the first slide and the next control on the last (via the native disabled attribute — <a> controls get aria-disabled instead, since they have no disabled IDL property). "wrap" jumps from the last slide back to the first, and vice versa. "loop" continues seamlessly past the ends for single-slide carousels, falling back to "wrap" for multi-item, peek, centered, or variable-width layouts, and under reduced motion. |
interval | number | 5000 | Milliseconds to wait before automatically advancing to the next item. |
keyboard | boolean | true | Whether Left/Right arrow keys navigate the carousel. |
pause | string, boolean | "hover" | "hover" pauses cycling on mouseenter and resumes it on mouseleave. false disables pause-on-hover. |
Methods
All Chassis CSS component methods are asynchronous and initiate CSS transitions. Methods return immediately when the transition begins, not when it completes. Calling methods on components that are already transitioning will be ignored to prevent conflicts. Learn more about Chassis JavaScript patterns.
Re-entrant calls are handled differently depending on what's in flight: navigating during a seamless loop transition is ignored until it finishes, while calling to()/next()/prev() during a plain scroll interrupts it and restarts toward the new target.
| Method | Description |
|---|---|
next() | Advances to the next slide. |
prev() | Moves to the previous slide. |
to(index) | Navigates directly to a slide by zero-based index. |
nextWhenVisible() | Calls next() only when the document and the carousel element are both visible. Returns true if it advanced, false otherwise. |
cycle() | Starts (or restarts) autoplay from the current slide. |
pause() | Stops autoplay; the indicator progress fill disappears until autoplay resumes. |
dispose() | Disconnects the instance's listeners and IntersectionObserver, cancels any in-flight scroll animation, and removes the stored instance data. |
getInstance() | Static. Returns the instance for an element, or null. |
getOrCreateInstance() | Static. Returns the existing instance or creates one. |
const carousel = new Carousel('#myCarousel', { interval: 2000 })
carousel.next()
carousel.prev()
carousel.to(2)
carousel.cycle()
carousel.pause()
carousel.dispose()
const instance = Carousel.getInstance('#myCarousel')
const instance = Carousel.getOrCreateInstance('#myCarousel')Events
All events fire on the .carousel element and are cancelable — calling event.preventDefault() on slide.cx.carousel blocks the transition, including a seamless loop transition.
| Event | Description |
|---|---|
slide.cx.carousel | Fires immediately when a transition begins. Cancelable. |
slid.cx.carousel | Fires once the transition completes and the new slide is active. |
Both events carry the same detail:
direction—"left"or"right", mirrored in RTL.relatedTarget— the.carousel-itembecoming active.from— the outgoing slide's index.to— the incoming slide's index.
const myCarousel = document.getElementById('myCarousel')
myCarousel.addEventListener('slide.cx.carousel', event => {
console.log(`Sliding from ${event.from} to ${event.to}`)
})CSS
The Carousel component can be customized at both runtime (via custom properties) and compile time (via Sass variables).
Custom properties
The Carousel component exposes CSS custom properties to control its appearance at runtime.
--cx-gap: var(--cx-carousel-gap, var(--cx-space-md));
--cx-indicator-gap: var(--cx-carousel-indicator-gap, 0.5rem);
--cx-indicator-idle-width: var(--cx-carousel-indicator-idle-width, 0.75rem);
--cx-indicator-idle-height: var(--cx-carousel-indicator-idle-height, 0.75rem);
--cx-indicator-idle-radius: var(--cx-carousel-indicator-idle-radius, var(--cx-border-radius-full));
--cx-indicator-active-width: var(--cx-carousel-indicator-active-width, 1.5rem);
--cx-indicator-active-height: var(--cx-carousel-indicator-active-height, 0.75rem);
--cx-indicator-active-radius: var(--cx-carousel-indicator-active-radius, var(--cx-border-radius-full));
--cx-indicator-idle-color: var(--cx-carousel-indicator-idle-color, var(--cx-default-fg-slight));
--cx-indicator-active-color: var(--cx-carousel-indicator-active-color, var(--cx-default-fg-main));
--cx-indicator-transition: var(--cx-carousel-indicator-transition, opacity 0.6s ease, width 0.3s ease);
--cx-overlay-inset: var(--cx-carousel-overlay-inset, auto 1rem 1rem);
--cx-overlay-color: var(--cx-carousel-overlay-color, var(--cx-white));
--cx-items: var(--cx-carousel-items, 1);
--cx-items-gap: var(--cx-carousel-items-gap, 0px);
--cx-items-peek: var(--cx-carousel-items-peek, 0px);
--cx-fade-duration: var(--cx-carousel-fade-duration, 0.6s);
--cx-fade-timing: var(--cx-carousel-fade-timing, ease);Sass variables
The Carousel component uses Sass variables in scss/config/_defaults.scss to define its defaults.
$carousel-gap: var(--space-md);
$carousel-items: 1;
$carousel-items-gap: 0px;
$carousel-items-peek: 0px;
$carousel-indicator-idle-width: $pagination-dot-idle-width * 1.5; //determines hit zone
$carousel-indicator-idle-height: $pagination-dot-idle-height * 1.5; //determines hit zone
$carousel-indicator-idle-radius: $pagination-dot-idle-radius; //determines hit zone
$carousel-indicator-active-width: $pagination-dot-active-width * 1.5; //determines hit zone
$carousel-indicator-active-height: $pagination-dot-active-height * 1.5; //determines hit zone
$carousel-indicator-active-radius: $pagination-dot-active-radius; //determines hit zone
$carousel-indicator-gap: $pagination-dot-gap;
$carousel-indicator-idle-color: $pagination-dot-idle-color;
$carousel-indicator-active-color: $pagination-dot-active-color;
$carousel-indicator-transition: opacity .6s ease, width .3s ease;
$carousel-overlay-inset: auto 1rem 1rem;
$carousel-overlay-color: var(--white);
$carousel-fade-duration: .6s;
$carousel-fade-timing: ease;