Appearance
Reusable Carousel
The carousel that powers the built-in Gallery, Team, Testimonials, Logo cloud, and Product carousel sections is a standalone Blade component pair — <x-filamentcraft::carousel> and <x-filamentcraft::carousel.slide> — that any custom section can reuse. It is a native horizontal scroller enhanced by the site bundle's JavaScript: arrow buttons, optional dot pagination, an aria-live position announcement, RTL-aware scrolling, and reduced-motion support, all following the APG carousel pattern (role="region" + aria-roledescription="carousel", one labelled slide per item).
Without JavaScript it degrades gracefully — the track is a plain scrollable flex row, and the arrow and dot controls stay hidden until the script confirms the content actually overflows.
The carousel component
blade
@php($headingId = 'fc-heading-'.$section->id)
<x-filamentcraft::carousel
:id="'my-carousel-'.$section->id"
:section="$section"
:labelledby="$headingId"
:columns="3"
:count="count($section->blocks)"
:dots="true">
...slides...
</x-filamentcraft::carousel>Props
| Prop | Default | What it does |
|---|---|---|
section | null | The current section object. Used to resolve the locale for translated labels and to derive a fallback id. |
id | auto | DOM id for the carousel (fc-carousel-{section id} or a random suffix). Pass one when a section can render several carousels. |
label | null | Accessible name for the carousel region and track (aria-label). |
labelledby | null | Id of an existing heading to use as the accessible name (aria-labelledby). Takes precedence over label — provide one of the two. |
locale | section / app locale | Locale the filamentcraft::filamentcraft.carousel.* labels resolve in. |
columns | 3 | Slides visible at the widest breakpoint (≥ 1280px). Capped at 3 from 1024px and 2 from 640px on the way down. |
gap | '1.25rem' | Gap between slides — becomes --fc-carousel-gap. |
peek | true | On mobile, size slides to 88% (92% when columns is 1) so the next slide peeks into view. false shows exactly one slide. |
arrows | true | Render previous/next buttons. |
dots | false | Render dot pagination (one dot per slide, built by the JS). |
count | null | Number of slides. Pass it so single-slide carousels skip the arrows, dots, and live-status element entirely. |
previous-label / next-label | translated | Override the arrow buttons' aria-labels. |
status | translated | Template for the aria-live announcement, with {start}, {end}, and {total} placeholders. |
Slots
Besides the default slot (the slides), the component accepts two named slots:
header— a heading/intro column rendered above the track; the arrows move up beside it.actions— extra controls (a "view all" link, say) rendered next to the arrows.
The slide component
Wrap every slide in <x-filamentcraft::carousel.slide> — it renders the role="group" / aria-roledescription="slide" wrapper and the flex-basis sizing class:
| Prop | What it does |
|---|---|
index / total | 1-based position, used to generate the localized "3 of 8" aria-label. |
label | Explicit aria-label, overriding the generated one. |
locale | Locale for the generated label. |
A complete example
The Team section's carousel branch, trimmed to the essentials — drop this into any custom section's Blade view:
blade
@php($headingId = 'fc-team-heading-'.$section->id)
<x-filamentcraft::carousel
:id="'fc-team-carousel-'.$section->id"
:section="$section"
:labelledby="$headingId"
:columns="(int) $columns"
:count="count($section->blocks)"
:dots="true">
@foreach ($section->blocks as $block)
<x-filamentcraft::carousel.slide
:index="$loop->iteration"
:total="count($section->blocks)"
:locale="$section->locale ?? null">
<div class="fc-card h-full">
{{-- your card markup --}}
</div>
</x-filamentcraft::carousel.slide>
@endforeach
</x-filamentcraft::carousel>Give cards h-full so every slide in a row matches the tallest one.
What the JavaScript does
The behaviour ships in the public site bundle (resources/js/site.ts → site.js) — nothing to register, and it survives Livewire morphs via a MutationObserver:
- Arrows scroll the track by exactly one slide width (plus gap), and are disabled at either end. The whole control row is hidden when the slides fit without scrolling.
- Dots are generated one-per-slide when
dotsis on, with a localized "Go to item N" label; the dot for the first visible slide is marked current. - Status — after each scroll settles, a visually hidden
aria-live="polite"element announces "Showing 2–4 of 9" (debounced, so mid-scroll positions aren't read out). - RTL — scroll direction flips automatically when the track renders in a right-to-left locale.
- Reduced motion —
prefers-reduced-motion: reduceswitches all programmatic scrolling from smooth to instant. - Keyboard — the track itself is focusable (
tabindex="0"), so native arrow-key scrolling works alongside the buttons.
CSS variables
The component computes these from its props and sets them inline on the track; override them from your own CSS (they live in the fc-carousel block of site.css):
| Variable | Set from | Purpose |
|---|---|---|
--fc-carousel-gap | gap | Space between slides. |
--fc-carousel-basis | peek / columns | Slide width below 640px (88% / 92% peek, or 100%). |
--fc-carousel-basis-sm | columns (max 2) | Slide width from 640px. |
--fc-carousel-basis-lg | columns (max 3) | Slide width from 1024px. |
--fc-carousel-basis-xl | columns | Slide width from 1280px. |
The visual pieces are plain classes you can restyle: .fc-carousel (the track), .fc-carousel__item, .fc-carousel__controls, .fc-carousel__button, .fc-carousel__header-actions, .fc-carousel__dots, and .fc-carousel__dot.
Translations
All user-facing strings come from filamentcraft::filamentcraft.carousel.* — previous, next, slide_label (:index of :total), status (Showing :start–:end of :total), go_to, and pagination. Publish the package lang files to change them, and see Localization & RTL for how section locales resolve.
Related
- Built-in Sections — the five sections with carousel layouts.
- Custom Sections — authoring the section around your carousel.
- Styling & Tailwind — how package CSS reaches your pages.
