shadimage
UI Beats
uibento-gridsource: ok · refreshed

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.

npx shadcn add "https://uibeats.com/r/bento-grid.json"
free

Where it goes

statsbento-grid

Visual style

bento

Interaction

animated-counter

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
animationmotion

Files1 · source cached 10/2/2026, 1:04:04 AM

components/demo/component/bento-grid.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;

More from UI Beats

Browse all components in this library →