Skip to content
goodtabs UI
BrowseBrowse components

Feature bento

A feature section laid out as a bento of mixed tiles on shared hairlines.

npx shadcn@latest add https://ui.goodtabs.co/r/feature-bento.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/feature-bento

Small parts, one system

  • Go to components
  • Copy install command
  • Toggle theme

Command menu

Search, jump and run actions from one input.

1.2 kB

Install size

Per component, gzipped.

4.5:1

Contrast

AA on every pair.

npx shadcn add @goodtabs/hero

Copy the source

No package to upgrade, the file is yours.

One 2x2 cell beside two numbers and a wide cell.

Tall and wide

One page, four jobs

Mix cell sizes to set the reading order.

Realtime

Updates land in under 200 ms.

  • Owner
  • Editor
  • Viewer

Roles

Owner, editor and viewer.

99.98%

Uptime

Last 90 days.

34

Regions

Edge locations, closest one answers.

9 min

Support

Median reply.

A wide chart cell, a tall list cell and two numbers.

Usage

import { FeatureBento, BentoCell } from "@/components/feature-bento"

<FeatureBento title="Small parts, one system">
  <BentoCell size="big" title="Command menu" description="Search and run actions.">...</BentoCell>
  <BentoCell title="Install size" description="Per component.">1.2 kB</BentoCell>
</FeatureBento>

Props

PropTypeDefaultDescription
titleReactNodeOptional section heading.
descriptionReactNodeOptional grey lead under the heading.
childrenReactNodeBentoCell elements.
size (BentoCell)"small" | "wide" | "tall" | "big""small"Footprint in columns by rows: small 1x1, wide 2x1, tall 1x2, big 2x2.

Source

feature-bento/feature-bento.tsx
import * as React from "react";
import { cn } from "@/lib/utils";

type FeatureBentoProps = Omit<React.ComponentProps<"section">, "title"> & {
  /** Optional section heading above the tiles. */
  title?: React.ReactNode;
  /** Optional grey lead under the heading. */
  description?: React.ReactNode;
  /** BentoCell elements. */
  children: React.ReactNode;
};

/**
 * A feature section built as a bento of mixed tiles. One frame paints the hairlines through a 1px gap,
 * each cell paints its own fill. Four columns from the container's 2xl width, one below. Server component.
 */
export function FeatureBento({ title, description, className, children, ...props }: FeatureBentoProps) {
  return (
    <section className={cn("@container w-full py-8 @2xl:py-16", className)} {...props}>
      <div className="mx-auto w-full max-w-6xl px-4 @2xl:px-6">
        {(title || description) && (
          <div className="mb-8 flex max-w-2xl flex-col gap-3 @2xl:mb-12">
            {title && (
              <h2 className="m-0 text-xl leading-tight font-medium tracking-tight text-balance @2xl:text-3xl">
                {title}
              </h2>
            )}
            {description && <p className="m-0 text-sm text-muted-foreground @2xl:text-base">{description}</p>}
          </div>
        )}
        <div className="surface-shine grid gap-px overflow-hidden rounded-lg border bg-border @2xl:grid-cols-4">
          {children}
        </div>
      </div>
    </section>
  );
}

const SPAN = {
  wide: "@2xl:col-span-2",
  tall: "@2xl:row-span-2",
  big: "@2xl:col-span-2 @2xl:row-span-2",
  small: "",
} as const;

type BentoCellProps = Omit<React.ComponentProps<"div">, "title"> & {
  /** Cell title, one short line. */
  title: React.ReactNode;
  /** One sentence under the title. */
  description?: React.ReactNode;
  /** Footprint: small is 1x1, wide 2x1, tall 1x2, big 2x2 columns by rows. */
  size?: keyof typeof SPAN;
  /** The visual above the text: a number, a list, a code line, a mini interface. */
  children?: React.ReactNode;
};

/** One cell of a FeatureBento: a visual on top, the title and a grey line below. */
export function BentoCell({ title, description, size = "small", className, children, ...props }: BentoCellProps) {
  return (
    <div
      data-slot="bento-cell"
      className={cn("flex min-w-0 flex-col justify-between gap-6 bg-card p-5 @2xl:p-6", SPAN[size], className)}
      {...props}
    >
      {children && <div className="flex min-h-0 flex-1 flex-col justify-center">{children}</div>}
      <div className="flex flex-col gap-1">
        <h3 className="m-0 text-sm font-medium text-foreground/85">{title}</h3>
        {description && <p className="m-0 text-sm text-muted-foreground">{description}</p>}
      </div>
    </div>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/feature-bento/prompt.md, or the Copy AI prompt button above.

When to use

  • A features section where some points deserve more room than others
  • When each cell has a small visual: a number, a list, a code line
  • Four to six cells that total a full rectangle

When not to use

  • Equal-weight points, which want the feature grid
  • The shared-hairline tiles with a title inside the card kit; use the bento grid for that
  • Cells with long copy, which stretch their row

Agent notes

  • Four columns from the container's @2xl width, one column below, where sizes are ignored
  • Cell sizes must tile a rectangle of four columns or the frame shows gaps filled with bg-border
  • The frame paints bg-border through a 1px gap and each cell paints bg-card, so cells must stay opaque
  • Server component; cells are plain divs, put your own visual in children

The maths

  • Grid = 4 columns, 1px gap; cell footprints big 2x2 = 4, wide 2x1 = 2, tall 1x2 = 2, small 1x1 = 1.
  • Demo one: 4 + 1 + 1 + 2 = 8 = 4 x 2, so the rectangle fills with no gap.