Shelf
Spotify-style horizontal row of cards with overlay navigation chevrons and edge-fade gradients, built on Carousel.
Clients ready for orchestration
New households or unfinished configuration.
<Shelf> <ShelfHeader> <ShelfTitle>Clients ready for orchestration</ShelfTitle> <ShelfDescription>New households or unfinished configuration.</ShelfDescription> </ShelfHeader> <ShelfContent itemsPerView={{ default: 1, sm: 2, md: 3, lg: 4 }}> {households.map((h) => ( <ShelfItem key={h.id}> <Card size="sm"> <CardHeader> <Avatar size="sm"> <AvatarFallback>{h.initial}</AvatarFallback> </Avatar> <CardTitle>{h.name}</CardTitle> <CardDescription>{h.note}</CardDescription> </CardHeader> <CardFooter> <Button variant="link" size="sm"> Set up <ArrowRight /> </Button> </CardFooter> </Card> </ShelfItem> ))} </ShelfContent></Shelf>Usage
import {
Shelf,
ShelfAction,
ShelfContent,
ShelfDescription,
ShelfHeader,
ShelfItem,
ShelfTitle,
} from "@reva/ui";Shelf is a thin opinionated composition over Carousel optimised for the most common Reva product pattern: a horizontally-scrollable row of cards with a section header, overlay navigation chevrons, and edge-fade gradients. Chevrons are hidden on small screens (touch-swipe only) and auto-hide on md+ when there is nothing more to scroll in that direction. The fade gradient blends from the surrounding surface to transparent, sourced from the --shelf-fade-from CSS variable (defaults to var(--background)); shelves placed inside a Card or muted section override per instance, e.g. <Shelf style={{ "--shelf-fade-from": "var(--card)" }}>.
Composition
Use the following composition to build a Shelf:
Shelf
├── ShelfHeader
│ ├── ShelfTitle
│ ├── ShelfDescription (optional)
│ └── ShelfAction (optional, right-aligned slot)
└── ShelfContent
├── ShelfItem
└── ShelfItemExamples
With action
Pair the header with ShelfAction to surface a "View all" link or any other right-aligned control. The action bottom-aligns with the description's last line.
Recently active households
Updated in the last 24 hours.
<Shelf> <ShelfHeader> <ShelfTitle>Recently active households</ShelfTitle> <ShelfDescription>Updated in the last 24 hours.</ShelfDescription> <ShelfAction> <Button variant="link" size="sm">View all</Button> </ShelfAction> </ShelfHeader> <ShelfContent itemsPerView={{ default: 1, sm: 2, lg: 4 }}> {/* …items… */} </ShelfContent></Shelf>Inside a Card
Override the fade source via --shelf-fade-from so the gradient blends with the parent surface. Here the parent is a Card (var(--card)).
Recent activity
The last six things that happened.
<Card> <CardContent> <Shelf style={{ "--shelf-fade-from": "var(--card)" }}> <ShelfHeader> <ShelfTitle>Recent activity</ShelfTitle> <ShelfDescription>The last six things that happened.</ShelfDescription> </ShelfHeader> <ShelfContent itemsPerView={3}> {/* …items… */} </ShelfContent> </Shelf> </CardContent></Card>Responsive itemsPerView
Pass a responsive object to adapt the visible count per breakpoint. Resize the viewport — count steps from 1 (mobile) → 4 (large desktop). Below md, chevrons hide and the row is touch-scrollable.
Strategies
<Shelf> <ShelfHeader> <ShelfTitle>Strategies</ShelfTitle> </ShelfHeader> <ShelfContent itemsPerView={{ default: 1, sm: 2, md: 3, lg: 4 }}> {/* …items… */} </ShelfContent></Shelf>Edge state — fewer items than itemsPerView
When the number of items is less than or equal to itemsPerView, both chevrons and edge fades hide automatically. The row reads as a static grid.
Today's tasks
<Shelf> <ShelfHeader> <ShelfTitle>Today's tasks</ShelfTitle> </ShelfHeader> <ShelfContent itemsPerView={4}> {/* three items only */} </ShelfContent></Shelf>Props
Shelf
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Merged onto the root <section>. |
| ...rest | React.ComponentProps<"section"> | — | Standard section attributes; use style to set CSS variables like --shelf-fade-from. |
ShelfHeader / ShelfTitle / ShelfDescription / ShelfAction
| Component | Notes |
|---|---|
ShelfHeader | CSS grid; auto-rows-min items-end gap-x-4 gap-y-1. Becomes grid-cols-[1fr_auto] when a ShelfAction is present. |
ShelfTitle | Renders Heading as="h3" size="lg" weight="semibold". Accepts all Heading props. |
ShelfDescription | Renders Text size="sm" color="muted-foreground". Accepts all Text props. |
ShelfAction | Right-aligned slot, bottom-aligned to the header (col-start-2 row-span-full self-end). Holds any element — typically Button variant="link" or IconButton. |
ShelfContent
| Prop | Type | Default | Description |
|---|---|---|---|
itemsPerView | number | { default, sm, md, lg, xl, 2xl } | { default: 1, sm: 2, md: 3, lg: 4 } | Fully-visible items per breakpoint. |
opts | EmblaOptionsType | — | Forwarded to the underlying Carousel. Defaults: align: "start", containScroll: "trimSnaps". |
plugins | EmblaPluginType[] | — | Forwarded to the underlying Carousel. |
setApi | (api: CarouselApi) => void | — | Forwarded to the underlying Carousel. |
className | string | — | Merged onto the relative-positioned wrapper. |
ShelfItem
Pass-through <div>. Inherits the responsive basis-* classes from ShelfContent's itemsPerView. Per-item override: <ShelfItem className="basis-1/2">…</ShelfItem>.