Components · Data display
Carousel
BetaScrolls through items, such as progress photos.
- Source
components/ui/carousel.tsx - shadcn/ui Carousel
- Figma: not linked yet (Q13)
Goblet squat
Romanian deadlift
Push-up
Seated cable row
Plank
Exercise 1 of 5
When to use
- To step through items of the same kind where one at a time is the point: the exercise demos in a workout, a client's progress photos in date order.
- When space is short, such as a card or a mobile screen, and coaches can be expected to look at the items in order.
When not to use
A carousel hides everything but the visible slide. Most people never get past the first one, and screen reader and keyboard users have to work to find the rest. Before using one, check whether all items could just be shown.
- For content coaches must see or compare: a list, a grid of Cards or a Table. Comparing a week 1 and a week 8 photo is easier side by side than two slides apart.
- For announcements, promotions or tips that rotate on their own: they are missed, and auto-rotation fails WCAG 2.2.2 unless it can be paused. This component has no autoplay; don't add it.
- For steps in a form or onboarding: use a stepper with a visible step count, so progress is clear.
- For a few items that fit on screen: show them.
Anatomy
Goblet squat
Push-up
Exercise 1 of 2
- 1. Region: role="region", aria-roledescription="carousel", with an aria-label
- 2. Slide: role="group", aria-roledescription="slide", labelled "1 of 5"
- 3. Previous button: hidden label from the UI labels, disabled at the start
- 4. Position: "Exercise 1 of 5", a live region (optional, recommended)
- 5. Next button: disabled at the end
By default the Previous and Next buttons sit outside the slides, 48px from each side (-start-12, -end-12), so leave that much room around the carousel. To place them below the slides, as in the anatomy, pass className="static translate-y-0".
Variants
| Variant | Use for |
|---|---|
| One slide in view (default) | Exercise demos: one at a time, with the cue to read. |
Several in view (basis-1/2, basis-1/3 on CarouselItem, opts={{ align: "start" }}) | Progress photos: the coach sees neighbours for context. |
Vertical (orientation="vertical") | Rarely. A vertical list is almost always better. |
States
| State | What changes | Token |
|---|---|---|
| First slide | Previous is disabled | opacity-(--opacity-disabled) |
| Last slide | Next is disabled | opacity-(--opacity-disabled) |
| Button hover | Outline button fill | hover:bg-accent (from Button) |
| Button focus-visible | Ring around the round button | ring-ring/50, 3px |
| Dragging | Slides follow the pointer or finger (Embla) | — |
Usage guidelines
- Always show the position ("Exercise 2 of 5") so coaches know how many there are.
- Keep slides the same height, and keep the controls in the same place on every slide.
- Put only short content in a slide, with at most one link or button. Long text and forms don't belong in a carousel.
- Give the carousel a name that says what it holds: "Exercise demos for Upper body A", not "Carousel".
Exercise 2 of 5
Content guidelines
- The carousel's label names the set and its owner: "Progress photos, Omar Haddad".
- Slide labels are positions: "1 of 6". In Arabic, "1 من 6".
- The position line uses the item's name: "Exercise 2 of 5", "Check-in 3 of 6".
- Exercise slides: name, dose ("3 × 12 reps · Rest 60 s") and one cue. Units follow Content › Units.
- Photo slides: the date and the measurement, so the photo is never the only information.
Accessibility
| Key | Does |
|---|---|
| Tab | Moves to the Previous and Next buttons, and to anything focusable inside the slides |
| Enter or Space on a button | Goes to the previous or next slide |
| Arrow left / Arrow right (focus anywhere inside the carousel) | Previous / next slide. Reversed in RTL: Arrow right goes to the previous slide |
- The root is a region with
aria-roledescription="carousel", so screen readers announce it as "Exercise demos for Upper body A, carousel". Without anaria-labelthe region has no name, so always pass one. - Each slide is a group with
aria-roledescription="slide". Label it with its position ("1 of 5"); the component doesn't add one. - The Previous and Next buttons have hidden labels from
lib/ui-labels.tsx: "Previous slide" and "Next slide", or "الشريحة السابقة" and "الشريحة التالية" on Arabic screens. They are 32px round, above the 24px minimum target. - Slides out of view are still in the tab order. Embla scrolls a slide into view when something in it gets focus, so keyboard users can reach every item, but keep focusable content in slides to one item.
- The visible position line is
aria-live="polite", so a slide change is announced ("Exercise 2 of 5"). The component doesn't announce changes on its own. - The arrow keys are captured on the whole carousel, including any text field inside a slide. Don't put inputs in slides.
RTL
- The carousel follows the reading direction: Embla gets
directionfrom Radix'sDirectionprovider, so in Arabic the first slide is on the right and the next one comes in from the left. - Previous sits on the right and Next on the left (
-start-12,-end-12), and their arrows mirror (rtl:-scale-x-100). - Arrow keys follow what you see: Arrow left goes to the next slide in RTL.
- Photos inside slides don't mirror.
Tokens used
Read from components/ui/carousel.tsx at build time, so this list changes when the component does. Opacity modifiers such as /90 are shadcn hover and focus tints on top of the token.
| Token | Utilities | SuperCoach | Coach |
|---|
- Radius
rounded-full
The carousel's colors come from the Button (outline variant) and from what you put in the slides.
Props
<Carousel>
Also takes every prop of <div>
| Prop | Type | Default |
|---|---|---|
| opts? | CarouselOptions | — |
| plugins? | CarouselPlugin | — |
| orientation? | "horizontal" | "vertical" | "horizontal" |
| setApi? | (api: CarouselApi) => void | — |
<CarouselContent>
Also takes every prop of <div>
No props of its own.
<CarouselItem>
Also takes every prop of <div>
No props of its own.
<CarouselPrevious>
Also takes every prop of <Button>
No props of its own.
<CarouselNext>
Also takes every prop of <Button>
No props of its own.
Replaces in coach-admin
The audit found no styles for this component in coach-admin.
The audit searched for carousel, Slick, Swiper and Embla classes and paths. src/reusableComponents/slider/ is not a carousel: it sets a fill width in percent, like a range or progress control.