Carousel
Slides the reader moves through, with autoplay that yields and controls that do not steal the frame.
A carousel is a strip of slides with one of them in view: a set of testimonials, three plans, a tour of a screen. It is an interaction rather than a surface, and what it owns is the moving, the index, the advance, the swipe and the two things a reader uses to leave the slide they are on.
What separates it from Tabs is that the content is sequential and passive: nothing here
changes what the page does, so the slides can advance on their own, and the two controls are a
pair of arrows over the frame rather than a row of named sections.
Usage
import { Carousel } from "@asheeui/web";
<Carousel
items={[
{ id: "testimonial", content: <Testimonial /> },
{ id: "plan", content: <PlanCard /> },
{ id: "statistic", content: <Statistic /> },
]}
defaultIndex={0}
/>
items is the shape a slide is described in: a content node and an optional id used as its
key. Children work too, <Carousel><Slide /><Slide /></Carousel> becomes the same array, but a
child has no identity of its own, so the position is its key.
defaultIndex is where an uncontrolled carousel starts and index with onIndexChange makes it
controlled: the component reports every move it would make and draws the index it is given, which
is what a screen that syncs the strip with a caption outside it needs. The two are exclusive in
practice, a controlled carousel ignores defaultIndex.
size is the frame's height rather than a control's density: sm is 15rem at its narrowest and
md, the registered value, 20rem. radius resolves from the application's defaultRadius and
falls back to lg, and variant is bordered, a two-pixel frame, or ghost, which keeps the
slides and drops the frame.
Autoplay, pausing and reduced motion
autoPlay is registered false, so nothing moves unless a screen asks it to. With it on, the
carousel advances every autoPlayInterval milliseconds, registered 5000, and pauses for four
reasons: a pointer resting on the frame, a reader tabbing into it, a drag in progress, and a
document with fewer than two slides. The first two are pauseOnHover, registered true.
Reduced motion is honoured. The component reads prefers-reduced-motion: reduce through
usePrefersReducedMotion and stops autoplay altogether rather than merely slowing it, and it
drops the transition so a change of slide lands instantly instead of travelling. Both are decided
by the document, not by the consumer: a screen that wants a carousel that never advances with
autoPlay off gets the same result, and a screen that wants the change of slide to land instantly
for everyone passes disableAnimation to the strip.
With loop false, autoplay stops at the last slide instead of wrapping, and each arrow disables
at its end, the registered loop is true, which is what a strip of testimonials wants and
what a set of numbered steps does not.
Controls and indicators
The registered frame shows both: two arrows drawn over the slides and a row of dots under them.
showControls and showIndicators (both registered true) remove either, and the pair is only
drawn at all when there is more than one slide, a single slide gets no affordance that would do
nothing.
renderPrevControl and renderNextControl replace a control's button with a consumer's,
receiving the handler and the disabled state. They replace the button and not its placement: the
framework draws its own arrows absolutely over the track, so a custom control that should sit over
a slide supplies its own positioning, and one that is left in the flow renders inside the frame,
under the track. controlClassName, indicatorClassName and itemClassName are the escape
hatches for the three parts.
Props
The registered values are size md, autoPlay false, autoPlayInterval 5000, loop
true, showControls true, showIndicators true and pauseOnHover true, behaviour rather
than dressing, so a product states its motion once under components.carousel.
disableAnimation is a cascade value like the rest, so components.carousel.disableAnimation
drops the slide transition for every carousel in the application; the prop wins over the section
when a single strip wants it anyway.
variant and radius are deliberately not registered: radius resolves from the
application's defaultRadius, falling back to lg, and variant resolves from the
application's defaultVariant onto the two frames a carousel draws. A carousel is only ever
bordered or ghost, so the shared vocabulary is mapped onto those two rather than looked up and
missed: solid, faded and underlined all land on the bordered frame, and ghost stays
ghost.
items, defaultIndex, index, onIndexChange, the two render functions and the three
class-name overrides are instance-only: which slides there are, where the strip starts and what
replaces an arrow are statements about one strip.
Accessibility
- The two arrows are ordinary
<button type="button">elements namedPrevious slideandNext slide, so they are reached by Tab and pressed with Enter or Space; each disables at its end when the carousel does not loop. - Each indicator is a button named for its destination,
Go to slide 1, and the current slide's dot carriesaria-current="true", so which slide is showing is announced as well as drawn. - Only the active slide is in the tab order and in the accessibility tree: the others are
inert(andaria-hiddenwhereinertis unsupported), so a reader tabs through the strip's own controls rather than through the content of slides they cannot see. The sighted transition is unchanged, the slides are still translated, not unmounted. - Autoplay stops when the document asks for reduced motion and while a reader is hovering or
focused, so a reader who tabbed in is not moved out from under themselves. There is no pause
button of the component's own: the way to stop the motion entirely is to leave
autoPlayoff.
Native
@asheeui/native renders a Carousel with the same props and a platform scroll view underneath,
so the swipe is the platform's paging rather than a pointer drag. The platform has no pointer
resting on anything, so pauseOnHover there means "pause while the reader is dragging", and
loop wraps the autoplay without wrapping the scroll, a reader who swiped to the end stays
there. The frame states Slide 2 of 3 as its own label, and the arrows are the characters ‹
and ›. See React Native.