WebberUI

Hover Expand Gallery

A horizontally squeezing expand gallery — panels change their share of the space on hover or keyboard focus, while the rest collapse and dim.

Loading preview…
npx shadcn@latest add https://webberui.com/r/hover-expand-gallery.json

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

5
1
8
<HoverExpandGallery />

Installation

npx shadcn@latest add https://webberui.com/r/hover-expand-gallery.json

Or, once registries are configured in components.json, install it as @webberui/hover-expand-gallery.

Usage

import {
  HoverExpandGallery,
  HoverExpandGalleryItem,
} from "@/components/ui/hover-expand-gallery";

<HoverExpandGallery className="h-64 w-full max-w-md" defaultActiveIndex={0}>
  <HoverExpandGalleryItem label="Ridge" description="The ridgeline at daybreak">
    <img src="/mountain.jpg" alt="" className="h-full w-full object-cover" />
  </HoverExpandGalleryItem>
  <HoverExpandGalleryItem label="Bay" description="Sandbars after the tide goes out">
    <img src="/bay.jpg" alt="" className="h-full w-full object-cover" />
  </HoverExpandGalleryItem>
</HoverExpandGallery>;

Each panel's share of the space is driven by its flex-grow weight: the expanded panel grows while the rest collapse proportionally. The container needs a height (horizontal orientation) or a width (vertical), and the panels fill it automatically.

Props

HoverExpandGallery

PropTypeDefaultDescription
childrenReactNodePut HoverExpandGalleryItem children here
activeIndexnumber | nullControlled expanded index; null means every panel collapses to an equal share
defaultActiveIndexnumber | null0Initial expanded index in uncontrolled mode; set null for equal shares by default
onActiveIndexChange(index: number | null) => voidFires when the expanded index is committed (hover does not fire it)
orientation"horizontal" | "vertical""horizontal"Layout direction: horizontal or vertical squeeze
expandOnHoverbooleantrueWhether to expand on cursor hover
expandedFlexnumber5flex-grow weight of the expanded panel
collapsedFlexnumber1flex-grow weight of a collapsed panel
gapnumber8Gap between panels (px)
minItemSizenumber44Minimum size of a collapsed panel (px)
aria-labelstring"互動圖庫"Accessible name for the gallery group (the built-in default is Traditional Chinese — pass this prop to name it in your own language)

HoverExpandGalleryItem

PropTypeDefaultDescription
childrenReactNodeFull-bleed background visual for the panel (image, gradient, and so on)
labelReactNodeTitle revealed on expand, which also serves as the accessible name
descriptionReactNodeSubtitle revealed on expand
iconReactNodeCorner identifier icon that stays visible when collapsed

How it works

  • The expanded state is decided by two layers, "hovered" and "committed (active)": hovering temporarily expands the hovered panel, and leaving returns to the active panel.
  • Clicking or keyboard-focusing a panel commits it as active, so when browsing with the keyboard the focused panel is the expanded one.
  • When activeIndex is out of range or set to null, every panel is laid out at collapsedFlex in equal shares.
  • An expanded panel reveals a gradient mask and title along its bottom edge, while collapsed panels get a translucent dark layer to sharpen the focus.

Accessibility

  • Each panel is a native <button> carrying aria-expanded to reflect its expanded state, with label (or description) as its accessible name.
  • The container is role="group" with an aria-label, overridable through the prop of the same name.
  • Keyboard: Tab moves focus panel by panel, arrow keys (←/→ when horizontal, ↑/↓ when vertical) move focus between panels, Home / End jump to the first and last; Enter / Space commits the expansion.
  • When the user has "reduce motion" enabled at the system level, panel sizes and reveals switch instantly with no transition animation.

On this page