Skip to contentAsheeUIAsheeUI

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 named Previous slide and Next 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 carries aria-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 (and aria-hidden where inert is 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 autoPlay off.

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.