# Bento Hover Expand — React Bento Grid

> Four tall tiles share a row; the hovered or focused one springs wide to reveal its detail and metric while its siblings narrow and dim.

- Category: [Bento Grids](https://pixelpapa.com/components/bento.md)
- Tags: bento, hover, expand, accordion, layout
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/bento/bento-hover-expand

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/bento-hover-expand.json"
```

The file is written to `components/pixelpapa/bento-hover-expand.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { ArrowUpRight, CalendarDays, Compass, Receipt, Users, type LucideIcon } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
import { cn } from "@/lib/utils";

type Panel = { icon: LucideIcon; title: string; body: string; stat: string; statLabel: string; tint: string };

const defaultPanels: Panel[] = [
  {
    icon: Compass,
    title: "Itineraries",
    body: "Drag flights, stays and dinners onto a shared timeline. Conflicts and tight connections are flagged before anyone books.",
    stat: "11 min",
    statLabel: "to plan a 3-city offsite",
    tint: "var(--chart-1)",
  },
  {
    icon: Users,
    title: "Group booking",
    body: "Collect passport names and seat preferences once, then book forty people in a single checkout with one invoice.",
    stat: "40 seats",
    statLabel: "in one transaction",
    tint: "var(--chart-3)",
  },
  {
    icon: Receipt,
    title: "Expenses",
    body: "Cards are issued per trip with limits per category. Receipts match themselves and leftovers return to the budget.",
    stat: "0",
    statLabel: "expense reports filed",
    tint: "var(--chart-5)",
  },
  {
    icon: CalendarDays,
    title: "Day-of support",
    body: "Missed connection at midnight? A human rebooks the whole group and updates every calendar while you sleep.",
    stat: "94 sec",
    statLabel: "median response, 24/7",
    tint: "var(--chart-4)",
  },
];

const spring = { type: "spring", stiffness: 240, damping: 30 } as const;

export default function BentoHoverExpand({ panels = defaultPanels }: { panels?: Panel[] }) {
  const [active, setActive] = useState(0);

  return (
    <section className="w-full bg-background px-6 py-20 sm:py-24">
      <div className="mx-auto max-w-6xl">
        <div className="flex flex-col justify-between gap-4 sm:flex-row sm:items-end">
          <h2 className="max-w-xl text-4xl font-semibold tracking-tight text-balance text-foreground sm:text-5xl">
            Team travel, minus the forty-tab spreadsheet
          </h2>
          <p className="max-w-xs text-sm text-muted-foreground">Ostara plans, books and pays for company trips. Hover a tile to look inside.</p>
        </div>

        <div className="mt-12 flex flex-col gap-3 md:h-[440px] md:flex-row">
          {panels.map((p, i) => {
            const open = active === i;
            return (
              <motion.button
                key={p.title}
                layout
                aria-expanded={open}
                onPointerEnter={() => setActive(i)}
                onFocus={() => setActive(i)}
                onClick={() => setActive(i)}
                animate={{ opacity: open ? 1 : 0.6 }}
                transition={spring}
                style={{ flexGrow: open ? 3.2 : 1, borderRadius: 24 }}
                className={cn(
                  "relative flex min-h-24 basis-0 flex-col overflow-hidden border bg-card p-6 text-left outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
                  open ? "min-h-72 border-transparent" : "border-border",
                )}
              >
                <motion.span
                  aria-hidden
                  animate={{ opacity: open ? 1 : 0 }}
                  className="absolute inset-0"
                  style={{ background: `radial-gradient(120% 90% at 100% 0%, color-mix(in oklch, ${p.tint} 26%, transparent), transparent 70%)`, boxShadow: `inset 0 0 0 1px color-mix(in oklch, ${p.tint} 45%, transparent)`, borderRadius: 24 }}
                />
                <motion.span layout="position" className="relative flex items-center gap-3">
                  <span className="flex size-10 shrink-0 items-center justify-center rounded-xl text-white" style={{ background: p.tint }}>
                    <p.icon className="size-5" />
                  </span>
                  <span className={cn("text-lg font-medium whitespace-nowrap text-card-foreground", !open && "md:hidden")}>{p.title}</span>
                </motion.span>

                <AnimatePresence initial={false} mode="popLayout">
                  {open && (
                    <motion.span
                      key="detail"
                      initial={{ opacity: 0, y: 16 }}
                      animate={{ opacity: 1, y: 0, transition: { delay: 0.12, duration: 0.4 } }}
                      exit={{ opacity: 0, transition: { duration: 0.1 } }}
                      className="relative mt-auto block w-full max-w-md pt-8"
                    >
                      <span className="block font-serif text-6xl leading-none text-card-foreground sm:text-7xl">{p.stat}</span>
                      <span className="mt-2 block text-sm text-muted-foreground">{p.statLabel}</span>
                      <span className="mt-6 block leading-relaxed text-pretty text-card-foreground/80">{p.body}</span>
                      <span className="mt-5 inline-flex items-center gap-1 text-sm font-medium text-primary">
                        See how it works <ArrowUpRight className="size-4" />
                      </span>
                    </motion.span>
                  )}
                </AnimatePresence>

                {!open && (
                  // Collapsed columns: the title runs bottom-to-top so it is never clipped.
                  <span className="relative mt-auto hidden items-end justify-between gap-3 md:flex">
                    <span className="rotate-180 text-xl font-medium whitespace-nowrap text-card-foreground [writing-mode:vertical-rl]">{p.title}</span>
                    <span className="rotate-180 font-serif text-2xl whitespace-nowrap text-muted-foreground [writing-mode:vertical-rl]">{p.stat}</span>
                  </span>
                )}
              </motion.button>
            );
          })}
        </div>
      </div>
    </section>
  );
}
```

## AI prompt

```text
Add the "Bento Hover Expand" component from PixelPapa UI to this project.

What it is: Four tall tiles share a row; the hovered or focused one springs wide to reveal its detail and metric while its siblings narrow and dim.
Category: Bento Grids. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/bento-hover-expand.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/bento-hover-expand.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/bento-hover-expand" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More bento grids

- [Bento AI Capabilities](https://pixelpapa.com/components/bento/bento-ai-capabilities.md): An AI feature bento: an answer streams word by word with citations, an agent ticks through its steps, a voice waveform pulses and a reply cross-fades between languages.
- [Bento Analytics](https://pixelpapa.com/components/bento/bento-analytics.md) (Pro): A metrics bento: an MRR curve that morphs between ranges, a donut whose legend isolates segments, a ticking live-visitor count, weekday bars and a row of sparklines.
- [Bento Collaboration](https://pixelpapa.com/components/bento/bento-collaboration.md): A multiplayer bento: named cursors glide across a shared board, comments pop into a thread, a teammate types live and reactions float up from a decision.
- [Bento Developer](https://pixelpapa.com/components/bento/bento-developer.md) (Pro): A developer bento: a terminal replays a deploy line by line, an API response swaps with the selected endpoint, a p95 latency graph scrolls live and uptime bars fill in.
- [Bento Product Overview](https://pixelpapa.com/components/bento/bento-product-overview.md) (Pro): Five product tiles that each run a tiny working demo: a scrubbable revenue line, a self-typing command bar, spring switches, a fanning avatar stack and a live timer.
- [Bento Security Globe](https://pixelpapa.com/components/bento/bento-security-globe.md) (Pro): A security bento around a slowly rotating dotted SVG globe with region pins and traffic arcs, flanked by a ticking threat counter, a key-rotation ring and a residency picker.
