Bento Grid
block / cards
An asymmetric feature grid where every card declares its own footprint in tracks, clamped so no span can widen the grid or scroll the page.
- type
- block
- files
- 1
- registry deps
- card, skeleton, @automindz/backdrop, @automindz/render-link
- npm deps
- lucide-react
installs
- components/automindz/bento-grid.tsx
- A span wider than the grid is clamped, never handed to CSS.
- Copy sits at the cell's floor, so a short card pads not stretches.
- The cta reveals on hover and on focus, and is always on under touch.
- A skeleton holds the card's declared footprint, so nothing reflows.
BentoGrid owns the tracks, BentoCard owns its footprint. A span is in **tracks**, not t-shirt sizes, and it is clamped to the grid's own column count before it reaches CSS — grid-column: span 4 in a three-track grid does not clamp itself, it widens the implicit grid and scrolls the page sideways. Cards must be **direct children** of the grid to receive that count; wrap the grid, not the card. rowHeight is a floor (minmax(rowHeight, auto)), never a fixed height. Copy sits at the bottom of the cell and is clamped at two title lines and three description lines — write to the clamp. background takes a node: pass <Backdrop pattern="mesh" /> from @automindz/backdrop, which installs with this item so no pattern is re-implemented here. href makes the whole cell one anchor and turns cta into decoration; action puts your own control in the cell, and the types reject both. Pass renderLink for next/link. No stylesheet, no client boundary.
Variations
one state per section
- 01Three columns, mixed footprints
- 02The hover reveal, from a keyboard
- 03A span the grid cannot honour
- 04Four columns, dense
- 05Loading, at the declared footprint
- 06Disabled, with the reason
- 07No cards at all
Three columns, mixed footprints
The default: 3 tracks from lg, 2 from sm, 1 below it. The lead card is 2×2 with a mesh backdrop behind its copy, the webhooks card is 2×1, and the rest are single cells. Hover any linked card and its call to action rises into view; the copy stays pinned to the bottom of the cell, so the short cards pad above the text instead of stretching it.
The hover reveal, from a keyboard
Tab into this grid. The call to action on the middle card is a real link and it appears the moment focus enters the card, not only under a cursor — the reveal is written inside a hover:hover + motion-safe block, so a touch screen and a reduced-motion visitor never see it hidden at all. The first card is a link, so its own label is aria-hidden decoration and there is exactly one tab stop for that cell.
A span the grid cannot honour
Both cards ask for four columns inside a two-column grid. The span is clamped to the tracks that exist rather than handed to CSS, which would widen the implicit grid and put a horizontal scrollbar on the page. Narrow the window past 640px and every card becomes one column wide, whatever it asked for.
Four columns, dense
grid-auto-flow: row dense, so the single-cell cards backfill the hole the 3×1 card leaves rather than pushing it down the grid. Worth knowing what it costs: dense packing moves cards out of source order visually, so the reading order and the tab order no longer match the layout.
Loading, at the declared footprint
Three of the five cards are still loading and each skeleton occupies exactly the cell its content will: same span, same row floor. Nothing in the grid moves when the data lands, which is the only reason a skeleton beats a spinner here.
Disabled, with the reason
A dashed outline rather than a shade, so the state survives a greyscale screenshot, and the reason is in the card instead of in a tooltip nobody on a phone can open. The anchor is not rendered at all — the cell is unreachable by keyboard rather than reachable and then talked out of activating — and the call to action goes with the interactivity.
No cards at all
Two grids are rendered here and only one of them is on the page: the second is mapped over an empty list and returns null, gaps included. A bento with the tracks but none of the cards reads as a failed load, so there is nothing below the first row.
