# Expandable Card

> Card that smoothly expands to reveal details using grid-rows animation, with aria-expanded and aria-controls.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/expandable-card
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/expandable-card.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/expandable-card-demo.tsx"
import { CalendarDays, Plane } from "lucide-react"

import { ExpandableCard } from "@/components/expandable-card"

export default function ExpandableCardDemo() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-4">
      <ExpandableCard
        icon={<Plane className="size-5" aria-hidden />}
        title="Flight to Lisbon"
        summary="Tue, 14 May · 08:45 – 11:20"
        defaultOpen
      >
        <dl className="grid grid-cols-2 gap-x-4 gap-y-3">
          <div>
            <dt className="text-xs">Gate</dt>
            <dd className="font-medium text-foreground">B24</dd>
          </div>
          <div>
            <dt className="text-xs">Seat</dt>
            <dd className="font-medium text-foreground">12A (window)</dd>
          </div>
          <div>
            <dt className="text-xs">Boarding</dt>
            <dd className="font-medium text-foreground">08:05</dd>
          </div>
          <div>
            <dt className="text-xs">Baggage</dt>
            <dd className="font-medium text-foreground">1 x 23 kg</dd>
          </div>
        </dl>
      </ExpandableCard>
      <ExpandableCard
        icon={<CalendarDays className="size-5" aria-hidden />}
        title="Team offsite"
        summary="3 days, 14 people"
      >
        Day one is workshops, day two is a hike with lunch at the ridge, and day three is open for demos and a
        retro. Dietary requirements are collected in the shared form.
      </ExpandableCard>
    </div>
  )
}
```

## Source

```tsx title="components/expandable-card.tsx"
"use client"

import * as React from "react"
import { ChevronDown } from "lucide-react"

import { cn } from "@/lib/utils"

export interface ExpandableCardProps extends Omit<React.ComponentProps<"div">, "title"> {
  title: React.ReactNode
  summary?: React.ReactNode
  icon?: React.ReactNode
  /** Revealed details */
  children: React.ReactNode
  defaultOpen?: boolean
  onOpenChange?: (open: boolean) => void
}

export function ExpandableCard({
  title,
  summary,
  icon,
  children,
  defaultOpen = false,
  onOpenChange,
  className,
  ...props
}: ExpandableCardProps) {
  const [open, setOpen] = React.useState(defaultOpen)
  const panelId = React.useId()

  return (
    <div
      className={cn(
        "w-full max-w-sm overflow-hidden rounded-2xl border bg-card text-card-foreground shadow-sm transition-shadow",
        open && "shadow-md",
        className
      )}
      {...props}
    >
      <button
        type="button"
        aria-expanded={open}
        aria-controls={panelId}
        onClick={() => {
          const next = !open
          setOpen(next)
          onOpenChange?.(next)
        }}
        className="flex w-full items-center gap-3 rounded-2xl p-5 text-left outline-none transition-colors hover:bg-accent/50 focus-visible:ring-4 focus-visible:ring-ring/40 focus-visible:ring-inset"
      >
        {icon && (
          <span className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
            {icon}
          </span>
        )}
        <span className="min-w-0 flex-1">
          <span className="block font-semibold leading-tight">{title}</span>
          {summary && <span className="mt-1 block text-sm text-muted-foreground">{summary}</span>}
        </span>
        <ChevronDown
          className={cn(
            "size-5 shrink-0 text-muted-foreground transition-transform duration-300 motion-reduce:transition-none",
            open && "rotate-180"
          )}
          aria-hidden
        />
      </button>
      <div
        id={panelId}
        role="region"
        inert={!open}
        className={cn(
          "grid transition-[grid-template-rows,opacity] duration-300 ease-out motion-reduce:transition-none",
          open ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0"
        )}
      >
        <div className="overflow-hidden">
          <div className="border-t px-5 pb-5 pt-4 text-sm text-muted-foreground">{children}</div>
        </div>
      </div>
    </div>
  )
}
```
