Bento Grid

A grid of tiles of different sizes that arrive one after another when the grid is reached.

Loading preview

Installation

npx shadcn@latest add @loomui/bento-grid

Usage

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

PropTypeDefaultDescription
staggernumber70Milliseconds between one tile arriving and the next.
startOnViewbooleantrueHold until the grid scrolls into view.
disabledbooleanfalseRender the finished layout with no arrival.
classNamestringnoneMerged onto the grid. Columns and gap here.

BentoCard

PropTypeDefaultDescription
titleReactNodenoneHeading for the tile.
descriptionReactNodenoneSupporting line under the title.
iconReactNodenoneMark shown above the title.
footerReactNodenonePinned to the bottom of the tile.
classNamestringnoneMerged 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.