VisorVisor
ComponentsData Display

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.

SoleSpark
StackedDefault

Stacked

Media on top with its own aspect ratio; body flows below as a sibling block.

SoleSpark
Overlay

Overlay

Body floats over the lower portion of the media.

Breaking change in v0.5.0: Default BentoTile.layout is now "stacked". Tiles that previously rendered body-over-media must explicitly pass layout="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).

Reference artwork
CoverDefault

Cover

Image fills the tile, cropping whichever dimension overflows.

Reference artwork
Contain

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 tileBentoTileMedia + BentoTileBody with meta / title / description (the default pattern shown above).
  • Headline tileBentoTileBody with BentoTileHeadline plus optional supporting copy. No media slot — the tile is the statement.
  • Text tileBentoTileBody with meta + title + description, no media.
  • Figure tileBentoTileFigure (chart, large number, custom SVG) replacing BentoTileMedia. Useful for infographics or stat callouts.
Manifesto

The fastest way to ship marketing-grade UI.

+92%
ConversionQ1 2026

Hero CTR uplift

Marketing-grade primitives lifted the homepage click-through rate from 4.1% to 7.9%.

Animal LA
WebArts District

Animal LA

Same animal, west coast. Coming summer 2026 to the LA Arts District.

Note2026

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-grid

This copies four files into your project:

  • components/ui/bento-grid/bento-grid.tsx — the component
  • components/ui/bento-grid/bento-grid.module.css — the styles
  • components/ui/bento-grid/bento-grid.module.css.d.ts — CSS module types
  • components/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

PropTypeDefaultDescription
colsnumber | ResponsiveValue<number>2Number of grid columns. Accepts a plain number or a responsive breakpoint map { base, sm?, md?, lg?, xl? }.
gapstring"4"Gap between tiles as a spacing token suffix. "4" resolves to var(--spacing-4).
revealbooleanfalseFade + rise each tile on viewport entry, staggered by DOM order. Suppressed under prefers-reduced-motion.
revealStepMsnumber110Per-tile delay in milliseconds. Each tile's actual delay is revealStepMs × index.
revealThresholdnumber0.2IntersectionObserver threshold for the entrance trigger.

Also accepts all standard <div> HTML attributes.

BentoTile

PropTypeDefaultDescription
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" | stringAspect 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.
hrefstringWhen provided, renders the tile root as an <a> element.
targetstringLink target. Only used when href is set.
relstringAuto-set when target="_blank"Link rel. Defaults to "noopener noreferrer" when target is _blank.

BentoTileMedia

PropTypeDefaultDescription
srcstringRequiredImage source URL.
altstringRequiredImage 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

PropTypeDefaultDescription
as"h1" | "h2" | "h3""h2"Heading element.

A larger display heading than BentoTileTitleclamp(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)

PropTypeDefaultDescription
itemsReactNode[]Array of items rendered as spans with · separators between siblings. When omitted, children are used as-is.

BentoTileTitle

PropTypeDefaultDescription
as"h2" | "h3" | "h4""h3"Heading level.
showArrowbooleanfalseRender 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.

SlotDefaultPurpose
--bento-tile-radiusvar(--radius-lg)Tile corner radius
--bento-tile-body-paddingclamp(1rem, 2vw, 1.75rem)Body padding
--bento-tile-body-gapvar(--spacing-3)Gap between meta / title / description
--bento-tile-media-scale-hover1.04Image scale on tile hover (set to 1 to disable)
--bento-tile-lift-hover0pxOptional 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

ComponentElementPurpose
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

  • BentoTile renders as a semantic <article> element by default, communicating self-contained content to assistive technologies.
  • Clickable tiles use an <a> element with href — never a div with an onClick handler.
  • BentoTileMedia requires a meaningful alt attribute. Use alt="" only for decorative images that are redundant with surrounding text.
  • External links (target="_blank") automatically receive rel="noopener noreferrer" unless overridden.
  • The hover arrow is aria-hidden — it's a visual affordance only.
  • Focus rings use var(--focus-ring-width) and var(--border-focus) tokens for theme consistency.
  • All hover transforms (image zoom, tile lift, arrow nudge) are suppressed under prefers-reduced-motion: reduce.