A grid of cards that open into their detail in place: the card widens into the panel over a soft veil, and ← → go from detail to detail.
Installation
When to use
- A grid of things with more to say than a card holds: projects, products, people, places. The detail opens where the card was, without a page or a dialog.
- Going through several in a row: open one, then ← → to the next, the panel reshaping as it goes.
When not to use
- A detail with a lot to do (forms, tabs, long lists): its own page holds it better.
- Cards that are only links: a plain grid of links is simpler.
Usage
Expanding Card with your own data: a whole component you can paste as a page.
"use client"
import { ExpandingCard } from "@/components/dashral/pro/expanding-card"
const projects = [
{ id: "relaunch", name: "Website relaunch", summary: "A new home, pricing and docs." },
{ id: "onboarding", name: "New onboarding", summary: "Three steps instead of seven." },
{ id: "mobile", name: "Mobile app 2.0", summary: "Offline boards and push for mentions." },
]
export default function Projects() {
return (
<ExpandingCard
items={projects}
getKey={(p) => p.id}
getLabel={(p) => p.name}
renderCard={(p) => <div className="p-5 font-medium">{p.name}</div>}
renderDetail={(p, { close }) => (
<div className="space-y-3 p-6">
<h3 className="text-xl font-semibold">{p.name}</h3>
<p className="text-sm text-muted-foreground">{p.summary}</p>
<button type="button" onClick={close} className="text-sm underline">
Close
</button>
</div>
)}
/>
)
}API reference
ExpandingCard
A grid of cards that open into their detail in place: the card widens into the panel at the grid's width, ← → go from detail to detail, and Escape or × shrinks it back onto its card.
| Prop | Type | Default |
|---|---|---|
items | T[]Required. | — |
getKey | (item: T) => stringRequired. | — |
getLabel | (item: T) => stringRequired. Its name, for screen readers ("Website relaunch"). | — |
renderCard | (item: T) => React.ReactNodeRequired. A card's face in the grid. | — |
renderDetail | (item: T, step: { close: () => void }) => React.ReactNodeRequired. Its detail, once open. | — |
columns | stringThe grid's columns; one on a phone, two from sm and three from lg by default. | sample |
open | string | nullOpen one from outside (by key), or follow which is open. | — |
onOpenChange | (key: string | null) => void | — |
className | string | — |
classNames | ExpandingCardClassNames | — |
type ExpandingCardClassNames
| Prop | Type | Default |
|---|---|---|
root | string | — |
grid | string | — |
card | string | — |
detail | string | — |
Keyboard
| Keys | Action |
|---|---|
| Tab | Reach the cards |
| ←or→or↑or↓ | Across the grid, by its columns |
| HomeorEnd | The first or last card |
| EnterorSpace | Open the card's detail |
| ←or→ | While open: the previous or next card's detail |
| Esc | Close; the focus returns to the card |
Accessibility
- Each card is a button named by getLabel that says it opens a dialog and whether it's open.
- The detail is a dialog (aria-modal) named after its card; the focus moves to its close button, and back to the card on close. The grid behind is hidden from screen readers while it's open.
- Previous, Next and Close are named buttons, so ← → have a pointer and screen reader way too.
Motion
- As Command Button opens: the card widens from its own shape (place, size, corner) into the panel on its row at the grid's width (0.42 s) over a soft veil, the detail fading in once there's room; closing, the detail leaves first and the panel shrinks back onto the card it shows now, the two crossfading there.
- Going from card to card, the detail slides the way you go while the panel's height follows it. The grid makes room at once when a detail is taller than it, and a detail out of sight scrolls into view.
- Smooth tweens on [0.4, 0, 0.2, 1], never springs. With reduced motion it only fades.