Carousel
Secondary items in short horizontal space: similar species, related products, photo sets, at least four. Base UI has no carousel, so it is composed from a Scroll Area in its rail kind (the track and a 2 px progress rail), a list of slides each labelled "n of N", Previous and Next buttons and a polite count. It never autoplays or loops, and it has no dots.
import { Carousel, CarouselPhoto, CarouselSlide } from '@fairgarden-private/design/components/Carousel'
The first slide aligns to the container's start edge and the end bleeds to the viewport edge (to the field's edge inside a forest or royal field), with the next slide peeking. Swipe, trackpad, the buttons and the arrow, Home and End keys on the focused track move it a slide at a time with snap; a vertical wheel never moves the track and scrolls the page. At either end that button is hidden with its space kept, and the count states the position. When every slide fits, the controls go and the slides sit as a static row.
Card carousels
Card carousels
kind="cards" holds CarouselSlides. Counts come from the carousel's own width and only rise with it: image and portrait cards run 2 + peek (1 + peek below 360 px); with post, editorial post cards run 1 at about 85% at base, 2 + peek from 768 px and 3 + peek from 1024 px. Without container queries the slide widths are percentages of the track, so the peek still holds.
Similar Species
1 of 6
Related Stories
1 of 5
import { Carousel, CarouselSlide } from '@fairgarden-private/design/components/Carousel'
import {
Card,
CardMedia,
CardMeta,
CardTitle,
CardTitleLink,
} from '@fairgarden-private/design/components/Card'
import styles from './cards.module.css'
const portrait =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 3 4'%3E%3Crect width='3' height='4' fill='%23b7c4a8'/%3E%3Ccircle cx='1.5' cy='1.6' r='.8' fill='%23627a55'/%3E%3C/svg%3E"
const landscape =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 3 2'%3E%3Crect width='3' height='2' fill='%23c9c1a6'/%3E%3Cpath d='M0 1.4 .9.9l.7.4.8-.6.6.5V2H0z' fill='%237a6d4a'/%3E%3C/svg%3E"
const species = [
['Hermit Thrush', 'Catharus guttatus'],
['Veery', 'Catharus fuscescens'],
['Swainson’s Thrush', 'Catharus ustulatus'],
['Gray-cheeked Thrush', 'Catharus minimus'],
['Bicknell’s Thrush', 'Catharus bicknelli'],
['American Robin', 'Turdus migratorius'],
]
const posts = [
['The river comes back', 'Feature · 12 min read'],
['What an easement protects', 'Explainer · 6 min read'],
['A year on the prairie', 'Photo essay · 9 min read'],
['Counting owls after dark', 'Field notes · 4 min read'],
['Seed libraries, explained', 'Explainer · 5 min read'],
]
/** Portrait species cards (2 + peek) and editorial post cards (`post`: 3 + peek from 1024 px of container). */
export function CarouselCards() {
return (
<div className={styles.stack}>
<Carousel
kind="cards"
label="Similar species"
header={<h3 className={styles.head}>Similar Species</h3>}
>
{species.map(([name, latin]) => (
<CarouselSlide key={name}>
<Card>
<CardMedia>
{/* A data-URI stand-in photo; next/image adds nothing here. */}
<img src={portrait} alt="" />
</CardMedia>
<CardTitle>
<CardTitleLink href="#species">{name}</CardTitleLink>
</CardTitle>
<CardMeta>
<i>{latin}</i>
</CardMeta>
</Card>
</CarouselSlide>
))}
</Carousel>
<Carousel
kind="cards"
post
label="Related stories"
header={<h3 className={styles.head}>Related Stories</h3>}
>
{posts.map(([title, meta]) => (
<CarouselSlide key={title}>
<Card>
<CardMedia>
<img src={landscape} alt="" />
</CardMedia>
<CardTitle>
<CardTitleLink href="#post">{title}</CardTitleLink>
</CardTitle>
<CardMeta>{meta}</CardMeta>
</Card>
</CarouselSlide>
))}
</Carousel>
</div>
)
}
Photo carousel
Photo carousel
kind="photos" holds CarouselPhotos: a photo figure whose caption stays visible, right-flush. Below 1024 px of container each slide is about 88% of the track. From 1024 px the carousel becomes a fixed strip: a 480 px track height, each slide as wide as its photo at that height (from ratio), so nothing is cropped, with 12 px gaps.
1 of 8
7 more images (example.org/wood-thrush/photos)
import { Carousel, CarouselPhoto } from '@fairgarden-private/design/components/Carousel'
function stand(width: number, height: number, sky: string, land: string) {
return `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 ${width} ${height}'%3E%3Crect width='${width}' height='${height}' fill='%23${sky}'/%3E%3Cpath d='M0 ${height * 0.7}L${width * 0.3} ${height * 0.45}L${width * 0.55} ${height * 0.62}L${width} ${height * 0.4}V${height}H0z' fill='%23${land}'/%3E%3C/svg%3E`
}
const photos = [
{
ratio: 3 / 2,
caption: 'Adult male in spring, singing from a low perch.',
credit: 'Photo: A. Díaz',
},
{
ratio: 4 / 5,
caption: 'Nest of dead leaves and mud in a sapling fork.',
credit: 'Photo: J. Okafor',
},
{
ratio: 16 / 9,
caption: 'Breeding habitat: mature eastern hardwood forest.',
credit: 'Photo: M. Chen',
},
{
ratio: 1,
caption: 'Juvenile, spotted on the back as well as the breast.',
credit: 'Photo: A. Díaz',
},
{ ratio: 3 / 2, caption: 'Foraging in leaf litter along a stream.', credit: 'Photo: R. Silva' },
{
ratio: 2 / 3,
caption: 'Winter range in the lowland forests of Central America.',
credit: 'Photo: L. Park',
},
{ ratio: 3 / 2, caption: 'Banding station, early May.', credit: 'Photo: J. Okafor' },
{ ratio: 5 / 4, caption: 'Eggs: three to four, pale blue.', credit: 'Photo: M. Chen' },
]
/** Eight photos: ≈ 88% single slides, and from 1024 px of container a 480 px strip at each photo's own ratio. */
export function CarouselPhotos() {
return (
<Carousel kind="photos" label="Wood Thrush photos" printUrl="example.org/wood-thrush/photos">
{photos.map((photo, index) => (
<CarouselPhoto
key={photo.caption}
src={stand(
Math.round(photo.ratio * 120),
120,
index % 2 ? 'b7c4a8' : 'c9c1a6',
index % 2 ? '627a55' : '7a6d4a',
)}
alt={photo.caption}
ratio={photo.ratio}
caption={photo.caption}
credit={photo.credit}
/>
))}
</Carousel>
)
}
In print
Controls, count, peek and rail are hidden. Card slides print as a 2-up grid; photo sets of up to 6 print 3-up at their own ratios. A larger photo set prints its first slide and caption and "n more images (printUrl)".
API Reference
Carousel
The carousel. Swipe, trackpad, the Previous and Next buttons, and the arrow, Home and End keys on the focused track all move it one slide at a time with snap. In print, card slides become a 2-up grid and photo sets of up to 6 a 3-up grid; a larger photo set prints its first slide and “n more images (short URL)".
| Prop | Type | Description |
|---|---|---|
label | | The carousel’s accessible name, e.g. “Similar species”. |
controlLabels | | Previous and Next accessible names. Default “Previous” and “Next”. |
header | | The module header (§11.8), above the track. |
kind | |
|
post | | Editorial post cards: 1 landscape slide at ≈ 85% at base, 2 + peek from
768 px, 3 + peek from 1024 px of container [D186]. Default |
primary | | Primary Radix scale: buttons, count, rail and thumb, and the slides’ inherited primary. Never defaulted; omitted, it inherits the scope [D133]. |
printUrl | | The short URL printed after a photo set of more than 6 slides: “5 more images (example.org/gallery)". No protocol. |
secondary | | Secondary Radix scale, passed to the slides; no carousel part uses it. Never defaulted. |
children | | The slides: |
CarouselSlide
One slide, labelled “n of N” for assistive technology. It snaps to its start; slides are focusable only through their own links.
| Prop | Type | Description |
|---|---|---|
ratio | | The photo’s width ÷ height (e.g. |
CarouselPhoto
A photo slide: a §8.5 photo Figure with the image at its own ratio and the caption right-flush under it.
| Prop | Type | Description |
|---|---|---|
alt | | The alternative text; empty only for a decorative photo. |
caption | | The caption: what to notice. Always visible, right-flush. |
credit | | The credit, in the same run: “Photo: Name / Program”. |
imgProps | | Image |
ratio | | The photo’s own width ÷ height (e.g. |
src | | The image source. |
Additional types
CarouselPhotoProps
Props for CarouselPhoto: the image, its ratio and the caption.
type CarouselPhotoProps = {
/** The image source. */
src: string;
/** The alternative text; empty only for a decorative photo. */
alt: string;
/** The photo's own width ÷ height (e.g. `3 / 2`), so the strip never crops it. */
ratio: number;
/** The caption: what to notice. Always visible, right-flush. */
caption?: React.ReactNode;
/** The credit, in the same run: "Photo: Name / Program". */
credit?: React.ReactNode;
/** Image `srcSet`, `sizes`, `loading` and other attributes. */
imgProps?: Omit<
React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>,
'src' | 'alt'
>;
}CarouselProps
Props for Carousel: section props, the kind (and post for cards), label, header, slides and the color axes.
type CarouselProps = CarouselCardsProps | CarouselPhotosPropsCarouselSlideProps
Props for CarouselSlide: li props and, for photo slides, the photo’s ratio.
type CarouselSlideProps = React.ClassAttributes<HTMLLIElement> &
React.LiHTMLAttributes<HTMLLIElement> & { ratio?: number }Specification: DESIGN-SYSTEM.md §12.4 (carousel; desktop counts and the photo strip per D186), §10.19 (scroll area) and §5.10.2 (module reflow).