Carousel
Carousels cycle through a set of panels (hero images, feature highlights, or onboarding steps). index is the first visible slide; slidesPerView controls how many fill the viewport.
Try it
Usage
Place RuiCarouselSlide children inside RuiCarousel. Each slide needs an id. Enable autoplay only when motion is not distracting and can be paused.
import { RuiCarousel, RuiCarouselSlide } from '@ecopages/radiant-ui/carousel';
<RuiCarousel index={0} transition="slide" showIndicators label="Feature highlights">
<RuiCarouselSlide id="first">First panel</RuiCarouselSlide>
<RuiCarouselSlide id="second">Second panel</RuiCarouselSlide>
</RuiCarousel>Custom markup
<rui-carousel> coordinates any light-DOM tree that matches its query contract. The
Rui* helpers stamp these targets; they are not required.
import '@ecopages/radiant-ui/carousel';
<rui-carousel index={0} transition="slide" slides-per-view={3} show-indicators label="Feature highlights">
<section data-ref="root" class="rui-carousel rui-carousel--slide" data-carousel-track="window" data-carousel-surface="cards" aria-roledescription="carousel" aria-label="Feature highlights">
<div class="rui-carousel__stage">
<div data-ref="viewport" class="rui-carousel__viewport">
<div data-ref="track" class="rui-carousel__track">
<div data-slide="first" class="rui-carousel__slide">First panel</div>
<div data-slide="second" class="rui-carousel__slide">Second panel</div>
<div data-slide="third" class="rui-carousel__slide">Third panel</div>
</div>
</div>
</div>
<div class="rui-carousel__footer">
<button type="button" data-carousel-action="prev">Previous</button>
<div data-ref="indicators" role="group" aria-label="Choose slide window"></div>
<button type="button" data-carousel-action="next">Next</button>
</div>
</section>
</rui-carousel>When show-indicators is set, the host creates [data-carousel-indicator] buttons inside [data-ref="indicators"]. One slide per view uses a tab for each slide. Multiple slides per view use one button per valid window start, with aria-current identifying the current window. Five slides with slidesPerView={3} have three window buttons. Arrow keys, Home, and End navigate those windows.
Show several slides
Set slidesPerView to how many slides fill the viewport (1.2 peeks the next slide at the start, and the previous slide at the end so the last full card sits on the right). slidesPerGroup is how far prev/next/autoplay move. index stays the first visible slide. One pane keeps chrome on the viewport; more than one slide paints each slide as a card. Space between cards is --rui-carousel-gap (default --space-inline):
rui-carousel {
--rui-carousel-gap: 1.5rem;
}<RuiCarousel transition="slide" slidesPerView={3} slidesPerGroup={3} label="Featured products">
<RuiCarouselSlide id="one">One</RuiCarouselSlide>
<RuiCarouselSlide id="two">Two</RuiCarouselSlide>
<RuiCarouselSlide id="three">Three</RuiCarouselSlide>
<RuiCarouselSlide id="four">Four</RuiCarouselSlide>
<RuiCarouselSlide id="five">Five</RuiCarouselSlide>
<RuiCarouselSlide id="six">Six</RuiCarouselSlide>
</RuiCarousel>Use autoplay sparingly
Autoplaying carousels can disorient users and violate reduced-motion preferences. Prefer manual controls and expose showRotationControl when autoplay is on.
Pick a transition
slide moves content horizontally. fade cross-fades panels. none swaps instantly and is best when motion would be gratuitous.
Set controlsVariant="overlay" to pin prev/next on the slide edges.
Theming
Carousel maps to semantic surface and border roles, never Tailwind palette steps:
| Part | CSS roles |
|---|---|
Viewport / card (rui-carousel__viewport or rui-carousel__slide) | surface, border, radius-container |
| Card gap | --rui-carousel-gap → --space-inline |
| Indicators | background, border |
| Active indicator | primary |
| Overlay chrome | surface, border, shadow-sm (blurred pills) |
| Focus | focus-ring |
| Geometry | --space-inline, --space-stack, --space-inset |
Override the --rui-carousel-* custom properties on rui-carousel. Theme roles stay the defaults behind those properties.
API
RuiCarousel is a custom element (<rui-carousel>) that owns slide state, autoplay, transitions, and control chrome. The RuiCarouselSlide and control helpers compose the surface. index is the first visible slide.
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
label | string | Carousel | Accessible name for the carousel region. |
index | number | 0 | First visible slide index. |
autoplay | boolean | false | Advance automatically. |
interval | number | 4000 | Autoplay interval in ms. |
transition | none · slide · fade | none | Slide swap animation. |
controls-variant | toolbar · overlay | toolbar | Chrome layout. |
show-indicators | boolean | false | Render slide tabs or multi-slide window buttons. |
show-rotation-control | boolean | false | Render play/pause control. |
loop | boolean | true | Allow looping past the last slide. |
wrap | boolean | true | Keep controls enabled at the ends. |
slides-per-view | number | 1 | Slides that fill the viewport. Values >= 1; fractional peek is allowed. At the end the last card sits on the right and the previous card peeks. |
slides-per-group | number | 1 | Slides moved per prev/next/autoplay/swipe. |
slide-count | number | 0 | Authored slide count for render-time control state. |
CSS properties
Set these on rui-carousel. Each defaults to a theme token.
| Property | Default | Used for |
|---|---|---|
--rui-carousel-gap | --space-inline | Space between cards (slides-per-view > 1) |
--rui-carousel-radius | --radius-container | Shell viewport or each card |
--rui-carousel-border-color | --border | Shell viewport or each card |
--rui-carousel-surface | --surface | Shell viewport or each card |
--rui-carousel-padding | --space-inset | Shell viewport (toolbar) or each card |
Light-DOM contract
| Target | Required | Host writes | Author owns |
|---|---|---|---|
[data-ref="root"] | yes | data-carousel-track (swap · stack · window), data-carousel-surface (shell · cards) | carousel region; view seeds aria-roledescription, aria-label |
[data-ref="viewport"] | yes | aria-live, aria-atomic | slide window |
[data-ref="track"] | yes | --rui-carousel-index, --rui-carousel-slides-per-view, --rui-carousel-gap-count, --rui-carousel-slide-count | slide track |
[data-slide] | yes | role, aria-*, aria-hidden, data-active, hidden (swap), id | slide id value and content |
[data-carousel-action="prev"] / next | no | disabled at ends | navigation buttons |
[data-carousel-action="rotation"] | no | aria-pressed, aria-label | play/pause control |
[data-ref="indicators"] | when show-indicators | role, aria-label; creates [data-carousel-indicator] | indicator container |
Do not set aria-hidden, aria-selected, or tabIndex on slides or indicators. Nested hosts: none.
View helpers
| Component | Target stamped | Notes |
|---|---|---|
RuiCarousel | shell targets via CarouselShell | slides convenience prop or RuiCarouselSlide children; default prev/next/rotation chrome. Stamps data-carousel-track and data-carousel-surface on [data-ref="root"]. |
RuiCarouselSlide | [data-slide] | Requires id prop. |
RuiCarouselPrev | [data-carousel-action="prev"] | Override with prev prop on RuiCarousel. |
RuiCarouselNext | [data-carousel-action="next"] | Override with next prop on RuiCarousel. |
RuiCarouselRotation | [data-carousel-action="rotation"] | Shown when autoplay or showRotationControl. |
CSS classes
Public BEM classes (documented via @cssclass on the element and view helpers):
| Class | Description |
|---|---|
.rui-carousel | Root region (aria-roledescription="carousel"). |
.rui-carousel--none | No animation between slides. |
.rui-carousel--slide | Horizontal slide transition. |
.rui-carousel--fade | Cross-fade transition. |
.rui-carousel--controls-toolbar | Controls below the viewport. |
.rui-carousel--controls-overlay | Controls overlaid on the viewport. |
.rui-carousel__stage | Viewport wrapper. |
.rui-carousel__viewport | Overflow-hidden slide window. |
.rui-carousel__track | Slide track. |
.rui-carousel__slide | Slide surface (RuiCarouselSlide). |
.rui-carousel__footer | Controls row below the viewport. |
.rui-carousel__toolbar | Toolbar grid. |
.rui-carousel__toolbar-rotation | Rotation control cell. |
.rui-carousel__toolbar-center | Indicators cell. |
.rui-carousel__toolbar-side--start | Prev control cell. |
.rui-carousel__toolbar-side--end | Next control cell. |
.rui-carousel__nav | Prev/next button (RuiCarouselPrev / RuiCarouselNext). |
.rui-carousel__nav--overlay | Circular on-slide chrome. |
.rui-carousel__nav--toolbar | Toolbar chrome (default). |
.rui-carousel__nav-label | Icon + label row (toolbar variant). |
.rui-carousel__nav-icon | Decorative chevron glyph. |
.rui-carousel__rotation | Rotation toggle button (RuiCarouselRotation). |
.rui-carousel__rotation--overlay | Overlay pill chrome. |
.rui-carousel__overlay-chrome | Absolute overlay layer over the viewport. |
.rui-carousel__overlay-rotation | Rotation control overlay position. |
.rui-carousel__controls--overlay | Prev/next overlay row. |
.rui-carousel__indicators | Indicator tablist. |
.rui-carousel__indicators--overlay | Overlay pill indicator tablist. |
.rui-carousel__indicator | Indicator button (role="tab"). |
Theme roles
| Part | CSS variables consumed |
|---|---|
| Viewport / card | --rui-carousel-surface, --rui-carousel-border-color, --rui-carousel-radius, --rui-carousel-padding |
| Card gap | --rui-carousel-gap |
| Indicators | --background, --border |
| Active indicator | --primary |
| Overlay chrome | --surface, --border, --shadow-control |
| Focus | --focus-ring |
| Geometry | --space-inline, --space-stack, --space-inset |
Accessibility
- Provide a descriptive
labelso screen readers identify the carousel region. - Prev/next controls must be keyboard reachable and expose their purpose in the accessible name.
- Slides outside the visible window are
aria-hidden.data-activemarks the first visible slide. - Respect
prefers-reduced-motionand avoid autoplay when users request reduced motion.