Bento Grid
An asymmetric tile grid primitive with full/half span variants, per-tile aspect ratios, contain/cover media fit modes, and marketing-grade typography sub-components.
Marketing Layout
The canonical layout for portfolio and showcase grids: media on top with its own aspect ratio, then an eyebrow → title → description body. Equal-height side-by-sides via CSS Grid's default align-items: stretch. Hover the tile to lift it -3px, zoom the image 1.04×, and nudge the title arrow.
Full-Width Hero Tile
A span="full" tile at 21/9 carries a single project across the row — the eyebrow, title (with hover arrow), and a longer description give the row weight.
Stacked vs Overlay
Two layout modes. Stacked (default) keeps media and body in normal document flow; total tile height = media + body. Overlay makes the tile carry the aspect ratio, the media fills it absolutely, and the body floats over the lower portion.
Stacked
Media on top with its own aspect ratio; body flows below as a sibling block.
Overlay
Body floats over the lower portion of the media.
Breaking change in v0.5.0: Default
BentoTile.layoutis now"stacked". Tiles that previously rendered body-over-media must explicitly passlayout="overlay"to retain that behavior.
Contain vs Cover
fit="cover" (default) fills the tile with the image; fit="contain" fits the image inside a surface-card background — ideal for logos, portraits, or artwork with transparent backgrounds. The hover zoom is suppressed in contain mode (letterboxed media doesn't benefit from a scale effect).
Cover
Image fills the tile, cropping whichever dimension overflows.
Contain
Image fits inside the tile; the surrounding plate fills the slack.
Responsive Asymmetric Layout
A fully responsive asymmetric grid — collapses to 1 column on mobile, 2 on medium+. Tiles in the same row share height regardless of body content length.
Tile Variants
BentoTile is a flexible container. Compose the sub-components inside to build distinct tile patterns:
- Project tile —
BentoTileMedia+BentoTileBodywith meta / title / description (the default pattern shown above). - Headline tile —
BentoTileBodywithBentoTileHeadlineplus optional supporting copy. No media slot — the tile is the statement. - Text tile —
BentoTileBodywith meta + title + description, no media. - Figure tile —
BentoTileFigure(chart, large number, custom SVG) replacingBentoTileMedia. Useful for infographics or stat callouts.
The fastest way to ship marketing-grade UI.
Hero CTR uplift
Marketing-grade primitives lifted the homepage click-through rate from 4.1% to 7.9%.
Animal LA
Same animal, west coast. Coming summer 2026 to the LA Arts District.
What's a bento?
Asymmetric tile grids descended from the Apple WWDC 2022 design language — mixed weights, aspect ratios, and spans inside a single grid scaffolding.
Entrance Animation
Pass reveal to BentoGrid to fade and rise each tile on viewport entry, staggered left-to-right by DOM order. Configure the per-tile delay with revealStepMs (default 110ms) and the trigger threshold with revealThreshold (default 0.2). The cascade is suppressed under prefers-reduced-motion: reduce.
<BentoGrid cols={2} reveal revealStepMs={140}>
{/* tiles */}
</BentoGrid>Installation
npx visor add bento-gridThis copies four files into your project:
components/ui/bento-grid/bento-grid.tsx— the componentcomponents/ui/bento-grid/bento-grid.module.css— the stylescomponents/ui/bento-grid/bento-grid.module.css.d.ts— CSS module typescomponents/ui/bento-grid/bento-grid.visor.yaml— component metadata
Usage
import {
BentoGrid,
BentoTile,
BentoTileMedia,
BentoTileBody,
BentoTileMeta,
BentoTileTitle,
BentoTileDescription,
} from '@/components/ui/bento-grid/bento-grid';
<BentoGrid cols={{ base: 1, md: 2 }} gap="4">
<BentoTile span="full" aspect="21/9" href="https://blacklight.fm">
<BentoTileMedia src="/projects/hero.jpg" alt="Blacklight hero" />
<BentoTileBody>
<BentoTileMeta items={['Web', 'Low Orbit', 'Live']} />
<BentoTileTitle showArrow>Blacklight</BentoTileTitle>
<BentoTileDescription>
Electronic press kits with an attitude, built for working artists.
</BentoTileDescription>
</BentoTileBody>
</BentoTile>
</BentoGrid>API Reference
No props data available for “bento-grid”.
BentoGrid
| Prop | Type | Default | Description |
|---|---|---|---|
cols | number | ResponsiveValue<number> | 2 | Number of grid columns. Accepts a plain number or a responsive breakpoint map { base, sm?, md?, lg?, xl? }. |
gap | string | "4" | Gap between tiles as a spacing token suffix. "4" resolves to var(--spacing-4). |
reveal | boolean | false | Fade + rise each tile on viewport entry, staggered by DOM order. Suppressed under prefers-reduced-motion. |
revealStepMs | number | 110 | Per-tile delay in milliseconds. Each tile's actual delay is revealStepMs × index. |
revealThreshold | number | 0.2 | IntersectionObserver threshold for the entrance trigger. |
Also accepts all standard <div> HTML attributes.
BentoTile
| Prop | Type | Default | Description |
|---|---|---|---|
layout | "stacked" | "overlay" | "stacked" | Visual layout. stacked places media on top with its own aspect ratio; overlay makes the tile carry the aspect ratio with the body floating over the media. |
span | "full" | "half" | number | "half" | Column span: full (1/-1), half (1 col), or a numeric span count. |
aspect | "21/9" | "2/1" | "16/10" | "4/3" | "1/1" | string | — | Aspect ratio. Applied to the media in stacked mode, to the tile root in overlay mode. |
fit | "cover" | "contain" | "cover" | Media fit mode. contain adds a surface-card background plate for logos/portraits. Hover zoom is suppressed in this mode. |
href | string | — | When provided, renders the tile root as an <a> element. |
target | string | — | Link target. Only used when href is set. |
rel | string | Auto-set when target="_blank" | Link rel. Defaults to "noopener noreferrer" when target is _blank. |
BentoTileMedia
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | Required | Image source URL. |
alt | string | Required | Image alt text (required for accessibility). |
loading | "lazy" | "eager" | "lazy" | Native browser image loading strategy. |
BentoTileBody
Accepts all standard <div> HTML attributes. In stacked mode the body sits at its natural content height (tiles hug content); in overlay mode it's pushed to the bottom with margin-top: auto and carries a translucent backdrop gradient for legibility against the image.
BentoTileFigure
Drop-in replacement for BentoTileMedia when the tile's hero element is not a photographic image — typical use cases: data charts, large statistic numbers, illustrated SVGs, or any composed JSX. Accepts all standard <div> HTML attributes. Inherits the same hover scale + layout-mode positioning as BentoTileMedia.
BentoTileHeadline
| Prop | Type | Default | Description |
|---|---|---|---|
as | "h1" | "h2" | "h3" | "h2" | Heading element. |
A larger display heading than BentoTileTitle — clamp(2rem, 3.6vw, 3.5rem), weight 700, tight tracking. Use for headline tiles where the heading IS the tile's primary content.
BentoTileMeta (eyebrow row)
| Prop | Type | Default | Description |
|---|---|---|---|
items | ReactNode[] | — | Array of items rendered as spans with · separators between siblings. When omitted, children are used as-is. |
BentoTileTitle
| Prop | Type | Default | Description |
|---|---|---|---|
as | "h2" | "h3" | "h4" | "h3" | Heading level. |
showArrow | boolean | false | Render a ↗ arrow after the title; the arrow nudges on tile hover. Useful when the tile is a link. |
BentoTileDescription
Renders a styled <p> with max-width: 56ch for comfortable reading.
Customization Slots
Each tile (.bentoTile) exposes the following CSS custom properties. Override per-tile via style or per-grid via a parent token-override scope.
| Slot | Default | Purpose |
|---|---|---|
--bento-tile-radius | var(--radius-lg) | Tile corner radius |
--bento-tile-body-padding | clamp(1rem, 2vw, 1.75rem) | Body padding |
--bento-tile-body-gap | var(--spacing-3) | Gap between meta / title / description |
--bento-tile-media-scale-hover | 1.04 | Image scale on tile hover (set to 1 to disable) |
--bento-tile-lift-hover | 0px | Optional vertical translate on anchor-tile hover. Default 0 — canonical affordance is the image scale + shadow bump. Override to e.g. -3px to opt in. |
Sub-components
| Component | Element | Purpose |
|---|---|---|
BentoGrid | <div> | CSS Grid container. Controls columns, gap, responsive breakpoints, and the entrance cascade. |
BentoTile | <article> or <a> | Individual tile. Polymorphic: renders as <article> by default, <a> when href is set. |
BentoTileMedia | <img> | Image element. Zooms 1.04× on tile hover (suppressed in fit="contain" mode). |
BentoTileFigure | <div> | Non-image hero slot — charts, numbers, custom SVGs. Inherits the same hover + layout behavior as BentoTileMedia. |
BentoTileBody | <div> | Body container. Hugs content in stacked mode; pushed to bottom in overlay mode. |
BentoTileMeta | <div> | Eyebrow row — uppercase, tracked, with · separators. Wraps tight when content overflows. |
BentoTileTitle | <h3> (or as) | Display heading. Optional hover arrow. |
BentoTileHeadline | <h2> (or as) | Larger display heading for headline-style tiles. |
BentoTileDescription | <div> | Muted body text at var(--font-size-sm), max-width: 56ch. |
Responsive Cols
All tiles collapse to a single column on narrow viewports (< 640px) regardless of the span prop. Use the responsive cols map to control layout at each breakpoint:
<BentoGrid cols={{ base: 1, sm: 2, md: 2, lg: 3 }}>
{/* tiles */}
</BentoGrid>Accessibility
BentoTilerenders as a semantic<article>element by default, communicating self-contained content to assistive technologies.- Clickable tiles use an
<a>element withhref— never a div with an onClick handler. BentoTileMediarequires a meaningfulaltattribute. Usealt=""only for decorative images that are redundant with surrounding text.- External links (
target="_blank") automatically receiverel="noopener noreferrer"unless overridden. - The
↗hover arrow isaria-hidden— it's a visual affordance only. - Focus rings use
var(--focus-ring-width)andvar(--border-focus)tokens for theme consistency. - All hover transforms (image zoom, tile lift, arrow nudge) are suppressed under
prefers-reduced-motion: reduce.