carousel-wc
Scroll carousel with prev/next buttons, dot indicators, autoplay, keyboard navigation, and ARIA.
Overview
A scroll-snap carousel with full progressive enhancement. Without JavaScript it renders as a simple scrollable row. With JS it adds prev/next buttons, dot indicators, autoplay, looping, keyboard navigation, and ARIA roles.
Usage
Wrap slide elements inside <carousel-wc>. Each direct child becomes a slide.
<carousel-wc> <div>Slide 1</div> <div>Slide 2</div> <div>Slide 3</div></carousel-wc>
Autoplay with Looping
Add autoplay and loop to cycle slides automatically. Autoplay pauses on hover and focus, and is disabled entirely when prefers-reduced-motion is active.
<carousel-wc autoplay loop autoplay-delay="3000"> <div>Slide A</div> <div>Slide B</div> <div>Slide C</div></carousel-wc>
Multi-item Carousel
Set item-width="auto" to show multiple items at once with a gap.
<carousel-wc item-width="auto" gap="m" loop> <div class="card">Card 1</div> <div class="card">Card 2</div> <div class="card">Card 3</div> <div class="card">Card 4</div></carousel-wc>
No Indicators
Set indicators="false" to hide dot indicators.
<carousel-wc indicators="false"> <div>Slide 1</div> <div>Slide 2</div></carousel-wc>
Attributes
| Attribute | Values | Default | Description |
|---|---|---|---|
autoplay | boolean | — | Auto-advance slides |
autoplay-delay | number | — | Delay between auto-advance in milliseconds |
loop | boolean | — | Loop back to start after last slide |
indicators | string | — | Show slide position indicators |
item-width | string | — | CSS width for each slide item |
gap | "xs", "s", "m", "l", "xl" | — | Gap between slides |
start | number | — | Initial slide index |
persist | string | — | localStorage key for position persistence |
transition | "fade", "slide", "scale" | — | View Transition animation between slides |
Required Structure
| Element | Required | Description |
|---|---|---|
any child elements | yes | Each direct child becomes a slide |
Events
const carousel = document.querySelector('carousel-wc'); carousel.addEventListener('carousel-wc:change', (e) => { console.log('Slide:', e.detail.index);}); carousel.addEventListener('carousel-wc:play', () => { console.log('Autoplay started');}); carousel.addEventListener('carousel-wc:pause', () => { console.log('Autoplay paused');});
JavaScript API
| Property / Method | Type | Description |
|---|---|---|
element.currentIndex | number | Current slide index (read-only) |
element.slideCount | number | Total number of slides (read-only) |
element.playing | boolean | Whether autoplay is active (read-only) |
element.next() | method | Go to next slide |
element.prev() | method | Go to previous slide |
element.goTo(index) | method | Jump to specific slide |
element.play() | method | Start autoplay |
element.pause() | method | Pause autoplay |
element.reset() | method | Return to initial slide, clear persistence |
const el = document.querySelector('carousel-wc'); el.next(); // Go to next slideel.prev(); // Go to previous slideel.goTo(2); // Jump to slide index 2 console.log(el.currentIndex); // Current slide indexconsole.log(el.slideCount); // Total slidesconsole.log(el.playing); // Autoplay active? el.pause(); // Pause autoplayel.play(); // Resume autoplayel.reset(); // Return to initial slide/code-block </section> <section> <h2>Keyboard Navigation</h2> <p>Focus the carousel track (Tab into it), then use arrow keys:</p> <table class="props-table"> <thead> <tr><th>Key</th><th>Action</th></tr> </thead> <tbody> <tr><td><kbd>ArrowLeft</kbd></td><td>Previous slide</td></tr> <tr><td><kbd>ArrowRight</kbd></td><td>Next slide</td></tr> <tr><td><kbd>Home</kbd></td><td>First slide</td></tr> <tr><td><kbd>End</kbd></td><td>Last slide</td></tr> </tbody> </table> </section> <section> <h2>Accessibility</h2> <h3>ARIA Roles</h3> <p>The component uses <code>role="region"</code> with <code>aria-roledescription="carousel"</code>. Each slide has <code>role="group"</code> with <code>aria-roledescription="slide"</code> and a label like "1 of 4". Dot indicators are simple buttons with <code>aria-current</code> marking the active slide.</p> <h3>Live Region</h3> <p>A visually hidden <code>aria-live="polite"</code> region announces slide changes to screen readers.</p> <h3>Reduced Motion</h3> <p>When <code>prefers-reduced-motion</code> is active, autoplay is completely disabled and scroll behavior uses instant transitions.</p> </section> <section> <h2>Styling</h2> <p>Override the button and dot styles using the <code>.carousel-prev</code>, <code>.carousel-next</code>, and <code>.carousel-dot</code> classes.</p> <code-block language="css" show-lines label="Custom styling" data-escape>/* Custom button styling */.carousel-prev,.carousel-next { background: var(--color-interactive); color: white; border: none;} /* Custom dot styling */.carousel-dot[data-active] { background: var(--color-primary);}
Progressive Enhancement
Without JavaScript, <carousel-wc> renders as a horizontal scrollable row using the :not(:defined) fallback. Once JS registers the component, controls and indicators appear.
/* Without JS: simple horizontal scroll */carousel-wc:not(:defined) { display: flex; overflow-x: auto; gap: var(--size-s); scroll-snap-type: x mandatory;} carousel-wc:not(:defined) > * { flex: 0 0 auto; scroll-snap-align: start;}
View Transitions
Add transition to switch from scroll-snap to View Transition animations. When active, the carousel uses a stacked-grid layout and animates between slides using the View Transitions API.
| Value | Effect |
|---|---|
fade (default) | Crossfade between slides |
slide | Directional slide — forward when advancing, backward when retreating |
scale | Scale down old slide, scale up new slide |
Dual-Mode Behavior
Without transition, the carousel uses scroll-snap with IntersectionObserver — slides are arranged in a horizontal row and the user scrolls between them. With transition, the carousel switches to a stacked CSS Grid layout where slides occupy the same grid cell and are shown/hidden with View Transition animations.
Related
<layout-reel>— CSS-only horizontal scroll with snap<tab-set>— Tabbed content switching<split-surface>— Resizable panel splitter