Content
Carousel
Page through cards that snap into place, with the next one peeking in.
import { Artwork } from "macbetter/artwork"
import {
Carousel,
CarouselContent,
CarouselIndicator,
CarouselIndicatorGroup,
CarouselItem,
CarouselNext,
CarouselPrevious,
CarouselViewport,
} from "macbetter/carousel"
const stories = [
{ id: "focus", eyebrow: "Get started", title: "Set up Focus for deep work", variant: "orbit", palette: "dusk" },
{ id: "photos", eyebrow: "Tips", title: "Find any photo by what's in it", variant: "bloom", palette: "sunset" },
{ id: "keychain", eyebrow: "Security", title: "Move to passkeys in five minutes", variant: "topo", palette: "forest" },
{ id: "journal", eyebrow: "Wellbeing", title: "Start a journal you'll keep", variant: "strata", palette: "berry" },
{ id: "shortcuts", eyebrow: "Power user", title: "Automate your morning with Shortcuts", variant: "prism", palette: "ocean" },
] as const
export default function Example() {
return (
<Carousel aria-label="Featured stories" style={{ width: "100%", maxWidth: 720 }}>
<CarouselViewport>
<CarouselContent>
{stories.map((story) => (
<CarouselItem key={story.id}>
<Artwork
seed={story.id}
variant={story.variant}
palette={story.palette}
aspect={16 / 10}
radius="var(--mb-group-radius)"
>
<span style={{ font: "var(--mb-text-footnote)", fontWeight: 600, textTransform: "uppercase", opacity: 0.85 }}>
{story.eyebrow}
</span>
<span style={{ font: "var(--mb-text-title-2)" }}>{story.title}</span>
</Artwork>
</CarouselItem>
))}
</CarouselContent>
</CarouselViewport>
<CarouselPrevious />
<CarouselNext />
<CarouselIndicatorGroup aria-label="Choose a story">
{stories.map((story) => (
<CarouselIndicator key={story.id} aria-label={story.title} />
))}
</CarouselIndicatorGroup>
</Carousel>
)
}When to use it
Use a carousel for a short row of peers people browse rather than read in order: featured stories, photos, product shelves. The next card peeks in from the edge, so it's clear there's more.
- Keep it to a handful of cards. Content people must see shouldn't be on the third page.
- Don't rotate on a timer unless the content is decorative; if you do, keep the play toggle.
- Use a horizontal Scroll View when items needn't snap to pages.
Usage
import {
Carousel,
CarouselContent,
CarouselIndicator,
CarouselIndicatorGroup,
CarouselItem,
CarouselNext,
CarouselPrevious,
CarouselViewport,
} from "macbetter/carousel"Put the cards in CarouselItems inside the viewport and content. CarouselPrevious and CarouselNext draw their chevrons and are named "Previous" and "Next" unless you pass your own aria-label. Write one CarouselIndicator per card, each named after its card; dots past the page count hide themselves.
Layout
layout="peek", the default, sizes each card to most of the view so the next one peeks in, like the App Store. layout="full" shows one page at a time. To size cards yourself, set --mb-carousel-item-size.
Platform behavior
- macOS. About two cards to a view, and glass arrow buttons that appear while the pointer is over the carousel. Trackpad swipes and the scroll wheel page it too.
- iOS. One card to a view, inset 16 points with the next card peeking in, and no arrows: you swipe. The page dots sit under the cards.
- iPadOS. Two cards to a view, with arrows when a pointer is connected.
- The arrows use the regular material, so they turn opaque under Reduce Transparency. In right-to-left layouts the cards run from the right and the arrows mirror.
- Arrows show only on devices with a fine pointer that hovers. Slides snap with the browser's scroll snapping, which follows the platform's own momentum.
Accessibility
- The carousel follows the WAI-ARIA carousel pattern, from ui-sdk. Name it with
aria-label, like "Featured stories". - The viewport takes focus, and Left and Right Arrow page through it. Cards out of view are inert, so Tab doesn't wander into them.
- The page dots are tabs: one tab stop on the current page, arrows to move between them, Home and End for the ends.
- Previous and Next stay focusable at the ends and report
aria-disabled.
API
Carousel
Takes every prop of ui-sdk's Carousel, including page, defaultPage, onPageChange, align, slidesPerPage, loop and controller, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
layout | "peek" | "full" | "peek" | Whether the next card peeks in, or one page shows at a time. |
CarouselViewport, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext, CarouselIndicatorGroup and CarouselIndicator wrap the ui-sdk parts of the same names.
Data attributes and CSS variables
| Attribute or variable | On | Meaning |
|---|---|---|
data-layout | .mb-carousel | peek or full. |
data-disabled | .mb-carousel-button | No page that way. |
data-selected | .mb-carousel-indicator | The current page's dot. |
--mb-carousel-item-size | platform | Each card's width. |
--mb-carousel-gap, --mb-carousel-inset | platform | Space between cards, and the inset before the first. |