Early bird: 30% off Dashral ProSee pricing

Dashral

Expanding CardPro

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.

example.tsx
"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.

PropTypeDefault
itemsT[]

Required.

—
getKey(item: T) => string

Required.

—
getLabel(item: T) => string

Required. Its name, for screen readers ("Website relaunch").

—
renderCard(item: T) => React.ReactNode

Required. A card's face in the grid.

—
renderDetail(item: T, step: { close: () => void }) => React.ReactNode

Required. Its detail, once open. close() closes it.

—
columnsstring

The grid's columns; one on a phone, two from sm and three from lg by default.

sample
openstring | null

Open one from outside (by key), or follow which is open.

—
onOpenChange(key: string | null) => void—
classNamestring—
classNamesExpandingCardClassNames—

type ExpandingCardClassNames

PropTypeDefault
rootstring—
gridstring—
cardstring—
detailstring—

Keyboard

KeysAction
TabReach the cards
←or→or↑or↓Across the grid, by its columns
HomeorEndThe first or last card
EnterorSpaceOpen the card's detail
←or→While open: the previous or next card's detail
EscClose; 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.