Add this component

npx zentauri-ui add bento-grid

Accessibility notes

Keyboard accessible by default with semantic markup, visible focus treatment, and tokenized states. Add descriptive labels for icon-only or decorative usage.

Dependency notes

Check Installation for shared peers. This component keeps styling in Tailwind classes and the --zui-* token contract.

Layout

Bento grid for dashboards and galleries

Use the bento grid component to build dashboard, gallery, and portfolio layouts from real CSS Grid — no absolute positioning. Three optional, cumulative animation layers add smooth reflow on filter and sort, hover-expanding bento items, and a shared-element morph into a detail view.

Active users

1x1 tile.

Uptime

Hover me — I grow to 2x1.

Latency

Tall 1x2 tile.

Errors

1x1 tile.

Alerts

Dense flow backfills gaps.

Bento grid variants playground

Pick columns, gap, animation level, and the featured item's span and appearance to preview the bento grid live. Reflow, bento-expand, and morph-to-detail are cumulative layers — toggle Show output / Show code and the snippet updates to match.


Featured

Hover to expand

Sessions

Conversion

Churn

Signups

NPS

All appearances

Every shipped appearance token at a glance. Click any swatch to load it into the playground above.

Bento Grid API

Generated from the package prop types and variant definitions.

BentoGrid

BentoGridProps

Variants

PropTypeDefault
autoFlow
denserow
dense
gap
lgmdsm
md

Behavior

PropTypeDefault
animationBentoGridAnimation | undefinednone
colsnumber | undefinednone
minItemWidthOverrides `cols` with `repeat(auto-fit, minmax(minItemWidth, 1fr))`.number | undefinednone
Inherited HTML props
PropTypeDefault
childrenReactNodenone
classNamestring | undefinednone
idstring | undefinednone
onClickMouseEventHandler<HTMLDivElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone

BentoGridItem

BentoGridItemProps

Variants

PropTypeDefault
appearance
bluecyandefaultemeraldglassgradient-bluegradient-greengradient-purplegraygreenindigolime+10
default
span
1x11x22x12x2featured
1x1

State

PropTypeDefault
onOpenDetail(() => void) | undefinednone

Behavior

PropTypeDefault
detailReactNodenone
expandableboolean | undefinednone
expandedSpanBentoGridSpan | undefinednone
onCloseDetail(() => void) | undefinednone
Inherited HTML props
PropTypeDefault
childrenReactNodenone
classNamestring | undefinednone
idStability key for reflow/morph animations. Must be unique within a grid.stringnone
onClickMouseEventHandler<HTMLDivElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone

BentoGridVariant

BentoGridVariantProps

Behavior

PropTypeDefault
appearance'default' | 'blue' | 'cyan' | 'green' | 'lime' | 'red' | 'slate' | 'zinc' | 'sky' | 'rose' | 'purple' | 'pink' | 'orange' | 'yellow' | 'teal' | 'indigo' | 'emerald' | 'gradient-blue' | 'gradient-green' | 'gradient-purple' | 'gray' | 'glass' | null | undefinednone
autoFlow'dense' | 'row' | null | undefinednone
gap'md' | 'sm' | 'lg' | null | undefinednone
span'1x1' | '2x1' | '1x2' | '2x2' | 'featured' | null | undefinednone

BentoGridAnimated

BentoGridAnimatedProps

animated

Behavior

PropTypeDefault
animationBentoGridAnimation | undefinednone
autoFlow'dense' | 'row' | null | undefinednone
colsnumber | undefinednone
gap'md' | 'sm' | 'lg' | null | undefinednone
minItemWidthOverrides `cols` with `repeat(auto-fit, minmax(minItemWidth, 1fr))`.number | undefinednone
refAllows getting a ref to the component instance. Once the component unmounts, React will set `ref.current` to `null` (or call the ref with `null` if you passed a callback ref).Ref<HTMLDivElement> | undefinednone
transitionPresetBentoGridTransitionPreset | undefinednone
Inherited HTML props
PropTypeDefault
childrenReactNodenone
classNamestring | undefinednone
idstring | undefinednone
onClickMouseEventHandler<HTMLDivElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone

BentoGridItemAnimated

BentoGridItemAnimatedProps

animated

State

PropTypeDefault
onOpenDetail(() => void) | undefinednone

Behavior

PropTypeDefault
appearance'default' | 'blue' | 'cyan' | 'green' | 'lime' | 'red' | 'slate' | 'zinc' | 'sky' | 'rose' | 'purple' | 'pink' | 'orange' | 'yellow' | 'teal' | 'indigo' | 'emerald' | 'gradient-blue' | 'gradient-green' | 'gradient-purple' | 'gray' | 'glass' | null | undefinednone
detailReactNodenone
expandableboolean | undefinednone
expandedSpanBentoGridSpan | undefinednone
onCloseDetail(() => void) | undefinednone
refAllows getting a ref to the component instance. Once the component unmounts, React will set `ref.current` to `null` (or call the ref with `null` if you passed a callback ref).Ref<HTMLDivElement> | undefinednone
span'1x1' | '2x1' | '1x2' | '2x2' | 'featured' | null | undefinednone
Inherited HTML props
PropTypeDefault
childrenReactNodenone
classNamestring | undefinednone
idStability key for reflow/morph animations. Must be unique within a grid.stringnone
onClickMouseEventHandler<HTMLDivElement> | undefinednone
styleCSSProperties | undefinednone
titlestring | undefinednone
CSS variable overrides

Bento grid CSS variables

Override these bento grid variables on :root, a theme selector, or a component wrapper.

119 variables

Pattern: --zui-<component>-<slot?>-<variant?>-<property>-<state?>-dark?

:root {
  --zui-bento-grid-row: minmax(7rem, auto);
  --zui-bento-grid-gap-sm: 0.5rem;
  --zui-bento-grid-gap-md: 1rem;
  --zui-bento-grid-gap-lg: 1.5rem;
  --zui-bento-grid-item-radius: 1rem;
  --zui-bento-grid-item-fg: oklch(20.8% 0.042 265.755);
  --zui-bento-grid-ring: oklch(44.6% 0.043 257.281);
  --zui-bento-grid-ring-offset: oklch(98.4% 0.003 247.858);
  --zui-bento-grid-default-border: #0000001a;
  --zui-bento-grid-default-bg: #0000000d;
  --zui-bento-grid-glass-border: #00000026;
  --zui-bento-grid-glass-bg: #0000001a;
  --zui-bento-grid-blue-border: #2563eb;
  --zui-bento-grid-blue-bg: color-mix(in oklch, #2563eb 10%, transparent);
  --zui-bento-grid-cyan-border: #0891b2;
  --zui-bento-grid-cyan-bg: color-mix(in oklch, #0891b2 10%, transparent);
  --zui-bento-grid-green-border: #16a34a;
  --zui-bento-grid-green-bg: color-mix(in oklch, #16a34a 10%, transparent);
  --zui-bento-grid-lime-border: #65a30d;
  --zui-bento-grid-lime-bg: color-mix(in oklch, #65a30d 10%, transparent);
  --zui-bento-grid-emerald-border: oklch(69.6% 0.17 162.48);
  --zui-bento-grid-emerald-bg: color-mix(in oklch, oklch(69.6% 0.17 162.48) 10%, transparent);
  --zui-bento-grid-indigo-border: oklch(39.8% 0.195 277.366);
  --zui-bento-grid-indigo-bg: color-mix(in oklch, oklch(39.8% 0.195 277.366) 10%, transparent);
  --zui-bento-grid-purple-border: oklch(43.8% 0.218 303.724);
  --zui-bento-grid-purple-bg: color-mix(in oklch, oklch(43.8% 0.218 303.724) 10%, transparent);
  --zui-bento-grid-pink-border: oklch(45.9% 0.187 3.815);
  --zui-bento-grid-pink-bg: color-mix(in oklch, oklch(45.9% 0.187 3.815) 10%, transparent);
  --zui-bento-grid-rose-border: oklch(64.5% 0.246 16.439);
  --zui-bento-grid-rose-bg: color-mix(in oklch, oklch(64.5% 0.246 16.439) 10%, transparent);
  --zui-bento-grid-sky-border: oklch(68.5% 0.169 237.323);
  --zui-bento-grid-sky-bg: color-mix(in oklch, oklch(68.5% 0.169 237.323) 10%, transparent);
  --zui-bento-grid-teal-border: oklch(70.4% 0.14 182.503);
  --zui-bento-grid-teal-bg: color-mix(in oklch, oklch(70.4% 0.14 182.503) 10%, transparent);
  --zui-bento-grid-yellow-border: oklch(79.5% 0.184 86.047);
  --zui-bento-grid-yellow-bg: color-mix(in oklch, oklch(79.5% 0.184 86.047) 10%, transparent);
  --zui-bento-grid-orange-border: oklch(70.5% 0.213 47.604);
  --zui-bento-grid-orange-bg: color-mix(in oklch, oklch(70.5% 0.213 47.604) 10%, transparent);
  --zui-bento-grid-red-border: #dc2626;
  --zui-bento-grid-red-bg: color-mix(in oklch, #dc2626 10%, transparent);
  --zui-bento-grid-slate-border: #475569;
  --zui-bento-grid-slate-bg: color-mix(in oklch, #475569 10%, transparent);
  --zui-bento-grid-gray-border: oklch(55.1% 0.027 264.364);
  --zui-bento-grid-gray-bg: color-mix(in oklch, oklch(55.1% 0.027 264.364) 10%, transparent);
  --zui-bento-grid-zinc-border: #52525b;
  --zui-bento-grid-zinc-bg: color-mix(in oklch, #52525b 10%, transparent);
  --zui-bento-grid-gradient-blue-border: oklch(42.4% 0.199 265.638);
  --zui-bento-grid-gradient-blue-from: oklch(97% 0.014 254.604);
  --zui-bento-grid-gradient-blue-to: oklch(97.7% 0.014 308.299);
  --zui-bento-grid-gradient-green-border: oklch(44.8% 0.119 151.328);
  --zui-bento-grid-gradient-green-from: oklch(96.2% 0.044 156.743);
  --zui-bento-grid-gradient-green-to: oklch(96.7% 0.067 122.328);
  --zui-bento-grid-gradient-purple-border: oklch(43.8% 0.218 303.724);
  --zui-bento-grid-gradient-purple-from: oklch(97.7% 0.014 308.299);
  --zui-bento-grid-gradient-purple-to: oklch(97.1% 0.014 343.198);
  --zui-bento-grid-overlay-bg: rgba(15,23,42,0.4);
  --zui-bento-grid-detail-max-w: 42rem;
  --zui-bento-grid-detail-radius: 1.25rem;
  --zui-bento-grid-detail-border: #0000001a;
  --zui-bento-grid-detail-bg: #ffffff;
  --zui-bento-grid-detail-fg: oklch(20.8% 0.042 265.755);
  --zui-bento-grid-detail-shadow: 0 18px 48px rgba(15,23,42,0.18);
  --zui-bento-grid-detail-close-fg: oklch(44.6% 0.043 257.281);
}

/* Dark theme variables follow the same names with -dark appended. */
.dark {
  --zui-bento-grid-item-fg-dark: oklch(98.4% 0.003 247.858);
  --zui-bento-grid-ring-dark: oklch(86.9% 0.022 252.894);
}

What it does

The bento grid renders items in a real CSS Grid with configurable columns (or container-driven auto-fit via minItemWidth), gaps, and dense auto-flow so small tiles backfill gaps left by larger spans. Items pick a span (1x1 through featured 3x2) and an appearance from the full palette, including gradients and glass.

Three cumulative animation layers are opt-in via the animation prop: reflow animates items to new positions on filter/sort/insert/remove, bento grows an item's span on hover or focus while neighbors reflow live, and morph shared-element-transitions a clicked item into an expanded detail view and back.

Composition and API

Compose BentoGrid with BentoGrid.Item children, each with a required stable id. The static entry (ui/bento-grid) needs no framer-motion; the animated entry (ui/bento-grid/animated) layers spring-driven motion on top with default, gentle, and snappy transition presets.

Items accept expandable + expandedSpan for the bento layer and a detail ReactNode for the morph layer, with onOpenDetail/onCloseDetail callbacks. Only one detail view can be open per grid — it's coordinated by grid-level context.

Common use cases

  • Build analytics dashboards with featured metric tiles that expand on hover.
  • Lay out image galleries and portfolio sections with mixed tile spans.
  • Compose admin panels where filtering and sorting reflow items smoothly.
  • Create marketing bento sections that morph tiles into detail panels on click.

Accessibility

The core constraint is structural: DOM order always equals visual order, so tab order matches what sighted users see. Expansion has keyboard parity (focus triggers it, not just hover), items with a detail view are reachable and operable with Enter or Space, and the detail dialog traps focus, closes on Escape, and returns focus to its trigger.

prefers-reduced-motion is honored automatically in the animated entry: items snap to their new state instantly instead of animating.

Next.js integration notes

Colocate examples under the App Router, keep server and client boundaries explicit, and pass detail-panel content as serializable JSX from server components.

Set NEXT_PUBLIC_SITE_URL so canonical and Open Graph URLs resolve on deploy.

FAQ

Does the Bento Grid component work without framer-motion?

Yes. The default static entry has zero framer-motion dependency: spans and appearances render fully, hover-expand still resizes items (it snaps via CSS instead of animating), and the detail view still opens and closes as a plain overlay. Import from the /animated subpath only when you want smooth reflow, FLIP-based expand, and the shared-element morph — framer-motion is an optional peer dependency.

Is the bento grid accessible for screen readers and keyboards?

Yes, by design. Items live in real CSS Grid, so DOM order always equals visual and reading order — unlike absolutely-positioned masonry libraries. Bento expand triggers on focus as well as hover, the detail view traps focus while open and returns focus to the triggering item on close, and prefers-reduced-motion automatically disables all motion with no opt-in required.

Does the Bento Grid support drag-to-reorder?

Not in v1. Drag-to-reorder is a candidate for a future dnd-kit-based add-on; keeping it out of v1 keeps the component dependency-light. Filtering, sorting, inserting, and removing items all animate via the reflow layer today.

Does the Bento Grid component work with Next.js App Router?

Yes. Both entries are client components; render them anywhere in the App Router and pass tile content (including the detail panel) as children or props from server components.