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 verifiedUses 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.jsonMethod 02 · Copy and paste
- 1. Install the dependencies below.
- 2. Copy the source from the Code tab into
components/ui/bento-grid.tsx. - 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.
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
| Prop | Type | Default | Description |
|---|---|---|---|
| items | BentoItem[] | — | { id: string; title: string; description?: string; span?: "1x1" | "2x1" | "1x2" | "2x2"; content?: ReactNode; href?: string }. |
| columns | number | 4 | Columns on containers ≥ 768px (2–4). Narrower containers use 2 (≥ 448px), then 1. |
| rowHeight | number | 170 | Minimum row height in px; rows grow if content needs more. |
| gap | number | 12 | Gap between tiles in px. |
| hoverEffect | "light" | "lift" | "both" | "none" | "light" | Pointer-following border light, a lift, both or none. |
| stagger | number | 70 | Delay between tiles in the entrance, in ms. |
| accent | string | "#FF7448" | Border light, link arrow and focus ring colour. |
| rounded | number | 20 | Tile corner radius in px. |
| headingLevel | 2 | 3 | 4 | 3 | Heading element used for tile titles. |
| ...props | HTMLAttributes<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
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.