COMPONENT / bento-grid
Brutal Bento Grid_
Hairline grid whose 2px gaps over a line-coloured parent become the borders. 1, 2 then 4 columns; cells span 1-4. Requires @cybr/tokens.
Install
1. Map the
@cybrnamespace, once per project. Add this inside yourcomponents.json, or into its existing"registries"object. Nocomponents.jsonyet? Runnpx shadcn@latest initfirst to create one."registries": { "@cybr": "https://cybr-ui.com/r/{name}.json" }2. Add components:
$ npx shadcn@latest add @cybr/bento-grid
If shadcn reports Unknown registry "@cybr", step 1 is missing from that project's components.json.
Preview
01 / SPAN 2
The grid is the border
02 / SPAN 1
2px gaps over a line-coloured parent.
03 / SPAN 1
Nothing floats.
04 / SPAN 4 · FULL ROW
registry/ui/bento-grid.tsx
import type { ComponentProps } from "react";
import { cn } from "@/lib/utils";
// Static class map so Tailwind sees every span. Spans of 3 and 4 fill the row
// at the 2-column breakpoint, then take their full width from lg.
const SPAN = {
1: "",
2: "sm:col-span-2",
3: "sm:col-span-2 lg:col-span-3",
4: "sm:col-span-2 lg:col-span-4",
} as const;
// Grid whose 2px gaps over a line-coloured parent become the borders: 1 column,
// then 2 from sm, then 4 from lg.
export function BentoGrid({ className, ...props }: ComponentProps<"div">) {
return (
<div
className={cn(
"grid grid-cols-1 gap-(--border-base) border-(length:--border-base) border-solid border-line bg-line sm:grid-cols-2 lg:grid-cols-4",
className,
)}
{...props}
/>
);
}
export function BentoGridItem({
span = 1,
className,
...props
}: ComponentProps<"div"> & { span?: 1 | 2 | 3 | 4 }) {
return (
<div
className={cn("flex min-w-0 flex-col gap-3.5 bg-bg px-7 py-8 sm:px-8", SPAN[span], className)}
{...props}
/>
);
}
Repo source. The shadcn CLI rewrites import aliases to match your project.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| BentoGrid ...props | ComponentProps<"div"> | — | The grid wrapper. Every native <div> attribute is forwarded; className is merged last with cn(). |
| BentoGridItem span | 1 | 2 | 3 | 4 | 1 | Columns the cell covers. 2 takes the full row from sm; 3 and 4 take the full row at sm, then 3 or 4 columns from lg. |
| BentoGridItem ...props | ComponentProps<"div"> | — | Every native <div> attribute is forwarded to the cell; className is merged last with cn(). |
Usage
Needs the stylesheet from @cybr/tokens. After the install, import it in app/globals.css:
@import "tailwindcss";
@import "../styles/cybr-tokens.css";
import { BentoGrid, BentoGridItem } from "@/components/ui/bento-grid";
export function Overview() {
return (
<BentoGrid>
<BentoGridItem span={2}>Spec card</BentoGridItem>
<BentoGridItem>Access card</BentoGridItem>
<BentoGridItem>Hardware panel</BentoGridItem>
<BentoGridItem span={4}>Stat band</BentoGridItem>
</BentoGrid>
);
}
Anatomy
- Grid: a
--linebackground showing through 2px (--border-base) gaps, so the gaps are the borders. A 2px--lineborder closes all four sides. The artboard is full-bleed and only shows top and bottom; a standalone component draws the full slab. - Columns: 1 column, 2 from
sm, 4 fromlg. These are viewport breakpoints, not container queries, so in a narrow container 4 columns can get tight. - Cells:
--bgfill, 28px/32px padding, and a column stack with a 14px gap. - Spans: a fixed class map, so every span class exists at build time.
Accessibility
- The grid is plain
<div>layout with no role. Give cells their own headings or landmarks when the content needs them. - Source order is reading order on every breakpoint; spans never reorder content.
- The gap lines are background colour, not content.
- Text you put in cells sits on
--bg:--inkis 14.6:1 and--label7.43:1. If you override colours withclassName, keep text contrast at 4.5:1 or above.