Skip to content

Expanding Panels

A row of panels where the one you point at opens and the others narrow.

From the Nitip template — see it in context
Aurora2026

Installation

$ npx shadcn@latest add https://framebrick.co/r/expanding-panels.json

Usage

import { ExpandingPanels } from "@/components/ui/expanding-panels";

<ExpandingPanels
  label="Recent projects"
  items={projects.map((p) => ({
    value: p.slug,
    label: p.title,
    meta: p.year,
    href: `/work/${p.slug}`,
    media: <Image src={p.cover} alt="" fill className="object-cover" />,
  }))}
/>

Props

PropTypeDefaultDescription
items{ value, media, label?, meta?, href? }[]—The panels. `media` is stretched to the panel and clipped; `href` makes it a Next.js link.
labelstring—Accessible name of the row.
valuestring—Open panel (controlled).
defaultValuestringfirst itemInitially open panel (uncontrolled).
onValueChange(value: string) => void—Called when another panel opens.
trigger"hover" | "click""hover"How a panel opens. Both settings also open on tap and on keyboard focus.
collapsedSizenumber130Width of a closed panel from 640 px up, in px.
heightnumber320Height of the row from 640 px up, in px.
  • Below 640 px the panels stack at full width and every label stays visible, as Framer does on phones.
  • Tabbing to a panel opens it, so the keyboard reaches the same content as the pointer.
  • Reduced motion is handled inside the component: flex-grow is neither a transform nor a size key, so MotionConfig does not skip it.
  • Each panel's button or link takes its accessible name from the label and meta (aria-labelledby), even while the label is faded out.

AI prompt

Paste into v0, Lovable, Bolt or Claude to rebuild this component in your own project.

Create a React + Tailwind CSS + Motion (motion/react) component called ExpandingPanels.
- A horizontal row of panels: each panel holds media (rounded-lg, overflow-hidden, bg-muted, children stretched) and,
  under it, a row with a label (text-sm font-medium, truncated) and optional meta (text-muted-foreground).
- Closed panels keep a fixed basis (default 130px) and flex-grow 0; the open one animates flex-grow to 1 with a spring
  (bounce 0, duration 0.4s). Its label animates opacity 0 → 1 with the same spring.
- A panel opens on pointer enter (mouse only), on click or tap, and on keyboard focus of its link or button.
  Controlled (value + onValueChange) or uncontrolled (defaultValue, first item by default).
- Under 640px the panels stack at full width, each with a fixed height, and every label stays visible
  (force opacity 1 with a class so the inline animation does not hide it).
- Reduced motion: flex-grow is not a transform or size key, so MotionConfig does not skip it — call useReducedMotion()
  and use a zero-duration transition instead.
- Accessibility: role group with an aria-label; each panel's media is a link or a button with aria-expanded, named
  by its label and meta through aria-labelledby (ids from useId; a panel without a label uses aria-label = value);
  visible focus ring (ring-[3px] ring-ring/50); a closed panel's label row is aria-hidden (the name still works).

Be the first to know

Get new templates and components first

One short email when we release a template or a free component. No spam — unsubscribe anytime.

We use your email only for Framebrick updates. Privacy policy