Skip to content
WANCRAFT

B.37Cards & Containers

Bento Grid

A responsive editorial bento layout with dense packing, a staggered entrance and pointer-lit tiles.

NewReactMotionCSSTailwindintermediate

Overview

Bento Grid lays out feature tiles of four sizes (1×1, 2×1, 1×2, 2×2) on a CSS grid with dense packing, so mixed spans fill the gaps on their own. Columns respond to the width of the container rather than the viewport, collapsing from four (or three) to two and then to a single column. Tiles stagger in once when the grid scrolls into view, and on hover each one can trace a pointer-following light along its border, lift, or both. Tiles with an href are real links with a visible focus ring.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/bento-grid.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/bento-grid.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/bento-grid.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { BentoGrid, type BentoItem } from "@/components/ui/bento-grid";

export function BentoGridExample() {
  const items: BentoItem[] = [
    { id: "sync", title: "Realtime sync", description: "Changes land everywhere in under 80ms.", span: "2x2" },
    { id: "edge", title: "Edge network", description: "Served from 300+ locations." },
    { id: "audit", title: "Audit log", description: "Signed, append-only history.", span: "1x2", href: "/docs/audit" },
    { id: "uptime", title: "99.99% uptime", description: "Measured over twelve months." },
    { id: "integrations", title: "Integrations", description: "Connect the tools you already use.", span: "2x1" },
  ];

  return (
    <BentoGrid items={items} />
  );
}

03Props

PropTypeDefaultDescription
itemsBentoItem[]—{ id: string; title: string; description?: string; span?: "1x1" | "2x1" | "1x2" | "2x2"; content?: ReactNode; href?: string }.
columnsnumber4Columns on containers ≥ 768px (2–4). Narrower containers use 2 (≥ 448px), then 1.
rowHeightnumber170Minimum row height in px; rows grow if content needs more.
gapnumber12Gap between tiles in px.
hoverEffect"light" | "lift" | "both" | "none""light"Pointer-following border light, a lift, both or none.
staggernumber70Delay between tiles in the entrance, in ms.
accentstring"#FF7448"Border light, link arrow and focus ring colour.
roundednumber20Tile corner radius in px.
headingLevel2 | 3 | 43Heading element used for tile titles.
...propsHTMLAttributes<HTMLDivElement>—Forwarded to the root (the size container).

04Features

  • 01Four tile sizes on a dense-packing CSS grid
  • 02Container-query columns: 4 / 3 → 2 → 1 based on available width, not the viewport
  • 03Staggered entrance that plays once when the grid enters the viewport
  • 04Pointer-following border light written to CSS variables (no re-renders), optional lift
  • 05Whole-tile links with an arrow cue
motion · staggermotion · tweeninput · viewportinput · hoverinput · pointerinput · focus

05Accessibility

  • Tiles are list items in a <ul role="list">, each with a real heading (configurable level).
  • Linked tiles are native <a> elements with an accent focus-visible ring; the arrow is aria-hidden.
  • Light layers are aria-hidden and ignore pointer events; the light only reacts to mouse input.
  • Tile order in the DOM matches the items array, so reading and tab order stay predictable even when dense packing moves tiles visually.
  • With prefers-reduced-motion tiles render immediately and the lift and fades are removed.

06Limitations

  • Dense packing can place a later tile visually before an earlier one; order items so the visual and DOM order roughly agree.