Carousel
The <x-wirekit:: 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 unavailable on the first slide and the Next button on the last slide. They are marked aria-disabled rather than disabled, so a keyboard reader who presses Next onto the last slide keeps the focus on it instead of losing their place on the page.
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.
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.
Behavior
- Native scroll snapping — the track is
overflow-x-auto snap-x snap-mandatory(or the-ypair when vertical) and the browser does the scrolling. Nothing is translated; there is no transition duration to tune, and the arrows and indicators callscrollTo - 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 |
|---|---|---|---|
label |
string|null |
null |
Accessible name for this slide. Undefaulted on purpose — an anonymous slide has no index of its own, so any built-in name would name every slide identically |
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 groups, each of which you name with label. 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"— a group, not a tab panel. Passlabelon every slide:aria-roledescriptionis unreliable on an element with no accessible name, so an unnamed track reads as N identical groups with nothing to tell them apart - Indicators: a
role="group"picker of buttons;aria-currentmarks the slide leading the view. The current dot is filled in the accent color and the others are rings in the strong border color, so every dot reaches 3:1 against the page and the current one differs in shape as well as color. Under forced colors, which removes the fill, the current dot is filled in the system's selection color - 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, which is under the 24×24 minimum of WCAG 2.5.8 Target Size (Minimum). They meet that criterion's spacing exception instead: the row puts their centers 24px apart, in either orientation, so the 24px circles the criterion draws around them touch without overlapping. The previous/next buttons are a second route as well: they are 40×40 at the default size, are rendered unconditionally, and reach every slide the dots reach.
If you build your own indicator row, keep small indicators at least 24px apart from center to center, or size each one to 24×24.
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 |
The four movement keys are the scroll container's own, not a handler this component adds. The
viewport is focusable and snaps mandatorily, so the browser resolves an arrow-key scroll to the
next slide and Home / End to the ends of the strip — which is why there is no key binding to
find in the source, and why the behavior is identical to any other snapping scroller your readers
already know.
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 |
Current indicator dot, fill and edge |
--color-wk-bg-elevated |
Nav-button background |
--color-wk-bg-hover |
Nav-button hover background |
--color-wk-border |
Nav-button border |
--color-wk-border-strong |
Edge of an indicator dot that is not current |
--color-wk-ring |
Focus ring (nav buttons + indicators) |
--ring-wk-width |
Focus ring width |
--border-wk-width |
Nav-button border width, and the indicator dots' edge (never under 1px) |
--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:::
use Pushery\WireKit\WireKit;
WireKit::personalize('carousel', [
'base' => fn (string $vendor): string => str_replace('rounded-[var(--radius-wk-lg)]', 'rounded-none overflow-hidden', $vendor),
]);
WireKit::personalize('carousel.slide', [
'base' => fn (string $vendor): string => $vendor.' p-8',
]);
Closures, because each is handed the block as it ships. A slide's block also carries snap-start, which makes the track settle on a slide, and the wk-carousel-slide marker the widths for per-view are set through; a plain string would replace them along with the rest of the block.
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