Carousel
The <x-wirekit::carousel> component creates a slide-based content rotator with autoplay, loop navigation, and indicator dots. It follows the WAI-ARIA Carousel pattern and announces slide changes to screen readers via a live region.
Usage
<x-wirekit::carousel>
<x-wirekit::carousel.slide>
<div class="p-12 text-center">
<h3>Slide 1</h3>
<p>Welcome to the carousel.</p>
</div>
</x-wirekit::carousel.slide>
<x-wirekit::carousel.slide>
<div class="p-12 text-center">
<h3>Slide 2</h3>
<p>Second slide content.</p>
</div>
</x-wirekit::carousel.slide>
<x-wirekit::carousel.slide>
<div class="p-12 text-center">
<h3>Slide 3</h3>
<p>Third slide content.</p>
</div>
</x-wirekit::carousel.slide>
</x-wirekit::carousel>
With Autoplay
Enable automatic slide advancement. The carousel pauses on hover and focus, then resumes on mouse leave and blur.
Without Loop
When loop is disabled, the Previous button is disabled on the first slide and the Next button is disabled on the last slide.
Image Carousel
Slides can contain any content — images, cards, or complex layouts:
With real images in your project:
<x-wirekit::carousel :autoplay="true" :interval="4000" aria-label="Product photos">
<x-wirekit::carousel.slide>
<img src="/images/product-1.jpg" alt="Product front view" class="w-full h-64 object-cover" />
</x-wirekit::carousel.slide>
<x-wirekit::carousel.slide>
<img src="/images/product-2.jpg" alt="Product side view" class="w-full h-64 object-cover" />
</x-wirekit::carousel.slide>
</x-wirekit::carousel>
Vertical Orientation
Set orientation="vertical" to scroll the slides up and down instead of left/right. The carousel pins a default viewport height (override it with a style="height: …" or a height class); each slide fills that viewport automatically, and the previous/next buttons move to the top and bottom edges.
Width & Layout
The carousel fills its parent width. Constrain it with Tailwind classes directly:
<x-wirekit::carousel class="max-w-2xl mx-auto">
<x-wirekit::carousel.slide>…</x-wirekit::carousel.slide>
</x-wirekit::carousel>
Slide height is determined by content. For fixed-height slides, apply height classes to the slide content.
Multiple slides per view
per-view shows more than one slide at a time — a product row, a logo rail, a
gallery strip. The browser scrolls and snaps; nothing is translated.
per-view takes 1–4. Each step keeps a narrower layout on small screens —
three-up on a 375px phone is three unreadable slivers — and shows part of the next
slide, which is what tells the reader there is a next slide.
It only applies horizontally. A vertical carousel is one slide tall by construction, so there is no second slot to share.
Naming the carousel
label is what a screen reader reads first, and it makes the carousel a landmark
someone can jump straight to:
<x-wirekit::carousel label="Customer stories">…</x-wirekit::carousel>
Name it for what it holds. "Carousel" is the default and says nothing.
Navigation
The carousel provides three navigation methods:
- Previous / Next buttons — circular buttons positioned at the left and right edges
- Indicator dots — clickable dot indicators at the bottom center
- Autoplay — automatic slide advancement with configurable interval
Autoplay Behavior
autoplay rotates the slides on a timer. When you turn it on, the carousel grows
a play/pause button — because anything that moves on its own for more than five
seconds needs a way to stop it (WCAG 2.2.2), and pausing on hover is not that way:
a touch user cannot hover, and neither can a keyboard user driving a screen reader.
<x-wirekit::carousel autoplay :interval="4000" label="Highlights">…</x-wirekit::carousel>
Hovering or focusing the carousel still pauses the rotation — but that is separate from the button. A mouse passing over the carousel does not make the button claim you paused it.
Reduced motion is handled for you
Under prefers-reduced-motion: reduce the carousel stays still until asked: the
auto-advance holds, and moving between slides jumps rather than glides. The play
button remains available, so a reader who wants the rotation can start it.
Keyboard
| Key | Action |
|---|---|
| Tab | Move focus to navigation buttons and indicator dots |
| Enter / Space | Activate the focused button or indicator |
Behavior
- CSS translate — slides move via
translateX()with a 500ms ease-in-out transition - Autoplay —
setIntervaltimer managed by Alpine; cleaned up indestroy() - Slide counting — total slides are counted from
[data-wk-carousel-slide]elements in the DOM - Livewire navigate — autoplay timer is cleared on
destroy()to prevent leaks during SPA navigation
Props
<x-wirekit::carousel>
| Prop | Type | Default | Description |
|---|---|---|---|
autoplay |
bool |
false |
Automatically advance slides |
interval |
int |
5000 |
Autoplay interval in milliseconds |
loop |
bool |
true |
Wrap around from last slide to first (and vice versa) |
orientation |
string |
'horizontal' |
'horizontal' (left/right) or 'vertical' (up/down; pins a default viewport height) |
perView |
int |
1 |
Slides visible at once — 1–4 (horizontal only; an unknown value degrades to 1) |
label |
string |
'Carousel' |
Accessible name for the carousel scroll region |
scope |
string|null |
null |
Scoped personalization key |
<x-wirekit::carousel.slide>
| Prop | Type | Default | Description |
|---|---|---|---|
scope |
string|null |
null |
Scoped personalization key |
Sub-Components
| Component | Purpose |
|---|---|
carousel.slide |
Individual content panel within the carousel |
Accessibility
This is the non-tabbed carousel pattern: a named region containing a scrollable
list of labeled groups. It is deliberately not a tablist — a tablist means exactly
one selected panel, and per-view shows several slides at once, so there would be no
single panel for a tab to point at.
- Container:
role="region",aria-roledescription="carousel",aria-labelfrom thelabelprop - Scroll region:
tabindex="0"with its own label, so the track is reachable and scrollable by keyboard - Slides:
role="group",aria-roledescription="slide"— each slide is a labeled group, not a tab panel - Indicators: a
role="group"picker of buttons;aria-currentmarks the slide leading the view - Nav buttons:
aria-label="Previous slide"/aria-label="Next slide" - Chevron SVGs:
aria-hidden="true"(decorative) - Live region:
aria-live="polite"announces "Slide X of Y" on every slide change - Disabled buttons: native
disabledattribute when loop is off and at first/last slide
Override the default label when the carousel content is not images — for example,
label="Testimonials" or label="Feature highlights". It is the first thing a screen
reader reads.
Indicator size on touch
The indicator dots are 8×8 and sit close together, which is under the 24×24 minimum of WCAG 2.5.8 Target Size (Minimum) and too close for its spacing exception. They rely on that criterion's equivalent control exception instead: the previous/next buttons are 40×40, are rendered unconditionally, and reach every slide the dots reach.
That means the dots are a shortcut, never the only route. If you build your own indicator row, either keep the built-in navigation buttons or size your indicators to 24×24 with 24px between their centers — do not ship the dots alone at this size.
Keyboard Interaction
| Key | Action |
|---|---|
Tab |
Move focus into the carousel |
ArrowLeft |
Move to the previous slide |
ArrowRight |
Move to the next slide |
Home |
Jump to the first slide |
End |
Jump to the last slide |
Enter / Space |
Activate the focused navigation button or indicator dot |
Pitfalls
- Don't use a carousel for primary content. WCAG research consistently finds <1% of users interact past the first slide. For must-see content use a hero with all items inline.
- Don't auto-rotate without a pause control. WCAG 2.2.2 — anything moving longer than 5s needs a user-controllable pause.
Design Tokens
| Token | Used for |
|---|---|
--font-wk-sans |
Caption font family |
--color-wk-text |
Caption text |
--color-wk-accent |
Active indicator dot |
--color-wk-bg-elevated |
Nav-button background |
--color-wk-bg-subtle |
Inactive indicator dot |
--color-wk-border |
Nav-button border |
--color-wk-ring |
Focus ring (nav buttons + indicators) |
--ring-wk-width |
Focus ring width |
--border-wk-width |
Border width |
--radius-wk-lg |
Slide + nav-button border radius |
--shadow-wk-md |
Nav-button drop shadow |
--padding-wk-x-sm / --padding-wk-y-sm |
Nav-button padding |
--opacity-wk-disabled |
Disabled indicator visual weight |
--transition-wk-duration |
Slide transition |
Personalization
Override classes globally via WireKit::personalize():
use Pushery\WireKit\WireKit;
WireKit::personalize('carousel', [
'base' => 'relative overflow-hidden rounded-none',
]);
WireKit::personalize('carousel.slide', [
'base' => 'w-full shrink-0 p-8',
]);
Further Reading
- WAI-ARIA Carousel Pattern — the authoring pattern this component implements
- WCAG 2.2.2: Pause, Stop, Hide — why autoplay pauses on hover/focus
- MDN:
aria-roledescription