Bento Grid
A grid of tiles of different sizes that arrive one after another when the grid is reached.
Installation
npx shadcn@latest add @loomui/bento-gridUsage
import { BentoCard, BentoGrid } from "@/components/ui/bento-grid"<BentoGrid>
<BentoCard
className="sm:col-span-2"
title="Copied, not installed"
description="Every component lands in your repo as one file you own."
/>
<BentoCard
title="Motion first"
description="Every piece animates for a reason."
/>
</BentoGrid>Tiles are spanned with the grid utilities you already know: sm:col-span-2,
row-span-2, whatever the layout needs. There is no size prop mapping names
onto spans, because that would be a second grammar for the one you have.
Arrival
The grid owns the arrival, not the tiles. It writes --bento-delay onto each tile to give it a place in the order. The
tile stays plain markup, so you can wrap it, reorder it or pull it into its own
component without dragging an index along behind it.
Nothing runs until the grid is on screen. A grid below the fold that has already played its arrival by the time you reach it has spent the effect on nobody, so it holds and waits.
<BentoGrid stagger={110} />stagger is the gap between one tile arriving and the next. Around 70ms reads
as one movement across the grid. Past about 150ms the last tile is arriving
well after the reader has started on the first one, and the grid feels slow to
assemble rather than deliberate.
Composing
BentoCard gives you a title, a description, an icon slot and a footer.
Children land between the description and the footer, so a chart or a mockup
drops straight in.
For the pointer highlight, wrap a tile in
spotlight-card rather than reaching for a
prop here. The grid handles layout and arrival. The highlight is its own
component and stays that way.
Props
BentoGrid
| Prop | Type | Default | Description |
|---|---|---|---|
stagger | number | 70 | Milliseconds between one tile arriving and the next. |
startOnView | boolean | true | Hold until the grid scrolls into view. |
disabled | boolean | false | Render the finished layout with no arrival. |
className | string | none | Merged onto the grid. Columns and gap here. |
BentoCard
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | none | Heading for the tile. |
description | ReactNode | none | Supporting line under the title. |
icon | ReactNode | none | Mark shown above the title. |
footer | ReactNode | none | Pinned to the bottom of the tile. |
className | string | none | Merged onto the tile. Spans go here. |
Any other <div> prop is forwarded.
Accessibility
Tiles are in the document from the start. Only opacity and offset animate, so
nothing is hidden from a screen reader while it waits its turn. Under prefers-reduced-motion the grid renders finished. Titles are
<h3>, so check that fits under whatever heading the section already has.