# Bento Grid

The BentoGrid component arranges cards of different sizes into the asymmetric layout known as a bento box, with each cell declaring how many columns and rows it spans and every card collapsing to full width on small screens.

- **Category:** Component
- **Docs:** https://uibeats.com/docs/component/bento-grid
- **Registry:** https://uibeats.com/r/bento-grid.json
- **License:** MIT
- **npm dependencies:** `motion`

## Installation

```bash
npx shadcn@latest add https://uibeats.com/r/bento-grid.json
```

Or install the dependencies and copy the source below:

```bash
npm install motion
```

## When to use

For a feature section where the items are not equal, since the layout is what ranks them. Keep spans to two or three columns.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | `-` | BentoCard elements to lay out (required) |
| `columns` | `number` | `3` | Columns from the sm breakpoint up. Below it the grid is always one column |
| `gap` | `number` | `16` | Space between cells, in pixels |
| `className` | `string` | `''` | Additional CSS classes for styling |
| `BentoCard: children` | `React.ReactNode` | `-` | Contents of one cell (required) |
| `BentoCard: colSpan` | `number` | `1` | Columns this card spans, from sm up |
| `BentoCard: rowSpan` | `number` | `1` | Rows this card spans, from sm up |
| `BentoCard: className` | `string` | `''` | Additional CSS classes for one cell |

## Example usage

```tsx
"use client";
import { BentoCard, BentoGrid } from "@/components/demo/component/bento-grid";

const CELLS = [
  {
    title: "Copy, paste, own it",
    body: "Every component lands in your repo as plain source you can edit.",
    colSpan: 2,
  },
  {
    title: "Motion included",
    body: "Animation is part of the component, not a wrapper bolted on later.",
    colSpan: 1,
  },
  {
    title: "Themed by default",
    body: "Colours come from your own tokens.",
    colSpan: 1,
  },
  {
    title: "Documented properly",
    body: "Every prop has a table, a playground, and a reason to exist.",
    colSpan: 2,
  },
];

const BentoGridUsage = () => {
  return (
    <BentoGrid columns={3} gap={12}>
      {CELLS.map(({ title, body, colSpan }) => (
        <BentoCard key={title} colSpan={colSpan}>
          <h3 className="text-sm font-semibold">{title}</h3>
          <p className="mt-1.5 text-sm text-muted-foreground">{body}</p>
        </BentoCard>
      ))}
    </BentoGrid>
  );
};

export default BentoGridUsage;
```

## Source

Save as `components/ui/bento-grid.tsx`.

```tsx
"use client";

import type { CSSProperties, ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

interface BentoGridProps {
  children: ReactNode;
  /** Columns from the `sm` breakpoint up. Below it the grid is always one. */
  columns?: number;
  /** Space between cells, in pixels. */
  gap?: number;
  className?: string;
}

interface BentoCardProps {
  children: ReactNode;
  /** Columns this card spans, from `sm` up. */
  colSpan?: number;
  /** Rows this card spans, from `sm` up. */
  rowSpan?: number;
  className?: string;
}

/**
 * An asymmetric grid of cards, in the layout everyone now calls a bento box.
 *
 * Both the column count and each card's span are driven by CSS custom
 * properties rather than inline `grid-column`, and both are gated behind the
 * `sm` breakpoint. That gate is the whole design: a card written as
 * `colSpan={2}` inside a one-column grid does not clamp to that column, it
 * makes the grid grow an implicit second one, so a naive inline span silently
 * shreds the layout on a phone. Holding the spans in variables that only apply
 * from `sm` up means every card is simply full width on small screens, and the
 * bento arrangement appears when there is room for it.
 */
export function BentoGrid({
  children,
  columns = 3,
  gap = 16,
  className = "",
}: BentoGridProps) {
  return (
    <div
      className={cn(
        "grid grid-cols-1 sm:[grid-template-columns:repeat(var(--bento-columns),minmax(0,1fr))]",
        className,
      )}
      style={{ "--bento-columns": String(columns), gap } as CSSProperties}
    >
      {children}
    </div>
  );
}

/**
 * One cell of a `BentoGrid`.
 *
 * Lifts on hover rather than scaling: a card that grows on hover overlaps its
 * neighbours, and in a grid whose whole point is that the cells are different
 * sizes, that reads as a layout bug.
 */
export function BentoCard({
  children,
  colSpan = 1,
  rowSpan = 1,
  className = "",
}: BentoCardProps) {
  const prefersReducedMotion = useReducedMotion();

  return (
    <motion.div
      className={cn(
        "relative overflow-hidden rounded-xl border bg-card p-5 shadow-subtle",
        "sm:[grid-column:span_var(--bento-col-span)] sm:[grid-row:span_var(--bento-row-span)]",
        className,
      )}
      style={
        {
          "--bento-col-span": String(colSpan),
          "--bento-row-span": String(rowSpan),
        } as CSSProperties
      }
      whileHover={prefersReducedMotion ? undefined : { y: -4 }}
      transition={{ type: "spring", stiffness: 320, damping: 26 }}
    >
      {children}
    </motion.div>
  );
}

export default BentoGrid;
```

## Credits

Written by [Claude Code](https://claude.com/claude-code).
