Duck Media Shelf
Rows of artwork from one array, with one tile width for the whole wall, skeletons in the tiles' own ratio, and an empty state for the library and for a single row.
Preview
Continue watching
New this week
Loads once, then holds its place.
My list
Installation
One command writes the section and every component it renders.
pnpm dlx shadcn@latest add @duck/duck-media-shelfThe block lands in components/blocks/duck-media-shelf.tsx. It builds on sticker-carousel, sticker-media-card, sticker-skeleton and empty-pond, which the CLI installs alongside it. Already installed items are skipped.
Usage
import { DuckMediaShelf } from "@/components/blocks/duck-media-shelf"A block is a starting point, not a widget. The props exist so the example renders with real content — once the file is in your project, hard-code what never changes and delete the rest.
Props
Everything not listed here is forwarded to the root element.
| Prop | Type | Default | Description |
|---|---|---|---|
| rows | DuckMediaShelfRow[] | - | title, description, actions, items, and per-row aspect, tileWidth, loading and emptyHint. Each row is a StickerCarousel. |
| aspect | "2/3" | "16/9" | "1/1" | number | "2/3" | Tile ratio for every row that does not override it. |
| tileWidth | number | 168 | Tile width in px, published as a CSS variable on the shelf. One number for the whole wall is what keeps two rows from drifting apart. |
| skeletonCount | number | 6 | Placeholder tiles per loading row. Match the page size you fetch. |
| empty | React.ReactNode | - | Replaces the default EmptyPond when every row is empty. |
| render | (item: DuckMediaShelfItem) => React.ReactElement | - | Return the framework's link element with no children. The tile is cloned into it through asChild, so the frame lands inside the anchor. |
Rules
What keeps this section from turning into noise.
- One tile width for the wall. A row that sizes slides with a basis per breakpoint drifts from the row under it the first time someone edits one of them.
- A skeleton takes the tile's ratio, not shape="poster". The ratio is a runtime value here, and a placeholder in the wrong shape moves the page when the artwork lands.
- Loading is aria-busy on the row, not a spinner. The row is already the shape it will be.
- An empty library gets the pond; an empty row gets one line of muted text. Four ducks on one screen is what happens when a row reuses the big empty state.
- The tile stays exactly one focusable link. Put a duration or a play badge in overlay, which takes no pointer events, rather than adding a control inside the frame.


