Bento grid
Grid of themed feature tiles that reveal one after another and reflow to four, three or two columns by orientation.
BentoGrid, features
Installation
npx shadcn add https://www.reelcn.dev/r/bento-grid.jsonFiles land in src/reelcn/ with the items it builds on.
Usage
<Center>
<BentoGrid
tiles={[
{ title: "Themes", body: "Six presets, one prop", span: 2, accent: true },
{ title: "Responsive", body: "16:9, 9:16 and 1:1" },
{ title: "Deterministic", body: "Seeded, frame-driven" },
]}
/>
</Center>Props
PropTypeDescription
delaynumberFrames to wait before entering.
durationnumberEnter duration in frames. Defaults to 0.6s. Some items reuse
duration for an unrelated local timing. See that item's own prop docs.exitnumber | booleanExit at the end of the parent `<Sequence>`.
false keeps it on screen, a number sets the exit length in frames.motionMotionPersonalityOverride the theme's motion personality.
posterbooleanRenders fully entered, with no exit: for a still frame or thumbnail. Defaults to
false.holdFramesnumberDelays the start of the exit window by this many frames, for a minimum on-screen hold. Defaults to
0.tiles*BentoTile[]
columnsnumberDefaults to 4 in landscape, 3 in square and 2 in portrait.
gapnumberSpace between tiles, in design units. Default
24.minHeightnumberMinimum tile height, in design units. Default
190.effectAnimateEffect Default
"up".stepnumberFrames between one tile's entrance and the next. Defaults to 0.1s.
backgroundstringTile fill. Defaults to the theme surface.
colorstringTitle color. Defaults to the theme foreground.
mutedColorstringBody color. Defaults to the theme muted color.
borderColorstringTile border. Defaults to the theme border.
accentColorstringFill of accent tiles. Defaults to the theme accent.
accentForegroundstringText on accent tiles. Defaults to the theme accent foreground.
styleCSSProperties
classNamestring
Built from
Use
- Product feature overviews, launch recaps and "what's new" slides
- Summing up four to eight short points on one screen
Avoid
- Cards in a single row or column, use
stagger
Guides
Components take their colors from a theme, their timing from motion props and their layout from the canvas.
Source
registry/items/bento-grid.tsx
/**
* @title Bento Grid
* @category motion
* @description Grid of themed feature tiles that reveal one after another and reflow to four, three or two columns by orientation.
* @duration data-driven
* @use Product feature overviews, launch recaps and "what's new" slides
* @use Summing up four to eight short points on one screen
* @avoid Cards in a single row or column, use `stagger`
* @tags bento, grid, features, tiles, cards
* @example
* <Center>
* <BentoGrid
* tiles={[
* { title: "Themes", body: "Six presets, one prop", span: 2, accent: true },
* { title: "Responsive", body: "16:9, 9:16 and 1:1" },
* { title: "Deterministic", body: "Seeded, frame-driven" },
* ]}
* />
* </Center>
*/
import type React from "react";
import { type AnimateEffect, effectStyle } from "./animate";
import { alpha, type MotionProps, staggerDelay, tween, useMotion, useSceneClock, useTheme, useViewport } from "./core";
export type BentoTile = {
title: string;
body?: string;
/** Columns the tile covers. */
span?: 1 | 2;
/** Paint the tile in the accent color. */
accent?: boolean;
};
export type BentoGridProps = MotionProps & {
tiles: BentoTile[];
/** Defaults to 4 in landscape, 3 in square and 2 in portrait. */
columns?: number;
/** Space between tiles, in design units. */
gap?: number;
/** Minimum tile height, in design units. */
minHeight?: number;
effect?: AnimateEffect;
/** Frames between one tile's entrance and the next. Defaults to 0.1s. */
step?: number;
/** Tile fill. Defaults to the theme surface. */
background?: string;
/** Title color. Defaults to the theme foreground. */
color?: string;
/** Body color. Defaults to the theme muted color. */
mutedColor?: string;
/** Tile border. Defaults to the theme border. */
borderColor?: string;
/** Fill of accent tiles. Defaults to the theme accent. */
accentColor?: string;
/** Text on accent tiles. Defaults to the theme accent foreground. */
accentForeground?: string;
style?: React.CSSProperties;
className?: string;
};
/** Column span per tile. A tile that no longer fits its row starts a new one and the tile before it widens to close the gap. */
function packSpans(spans: number[], columns: number) {
const packed: number[] = [];
let used = 0;
for (const wanted of spans) {
const span = Math.min(wanted, columns);
if (used + span > columns) {
packed[packed.length - 1] += columns - used;
used = 0;
}
packed.push(span);
used += span;
}
if (packed.length > 0) packed[packed.length - 1] += columns - used;
return packed;
}
export function BentoGrid({
tiles,
columns,
gap = 24,
minHeight = 190,
effect = "up",
step,
background,
color,
mutedColor,
borderColor,
accentColor,
accentForeground,
style,
className,
...motion
}: BentoGridProps) {
const theme = useTheme();
const { u, orientation } = useViewport();
const m = useMotion(motion);
const sceneClock = useSceneClock();
const cols = columns ?? (orientation === "landscape" ? 4 : orientation === "square" ? 3 : 2);
const spans = packSpans(
tiles.map((tile) => tile.span ?? 1),
cols,
);
const frames = step ?? Math.round(m.fps * 0.1);
const delayFor = (index: number, count: number) =>
sceneClock?.delayFor(index, count) ?? staggerDelay(index, count, { step: frames });
return (
<div
className={className}
style={{
display: "grid",
gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
gridAutoRows: `minmax(${u(minHeight)}px, auto)`,
gap: u(gap),
width: "100%",
maxWidth: u(cols * 400),
fontFamily: theme.fonts.body,
opacity: 1 - m.exit,
scale: String(1 - 0.03 * m.exit),
...style,
}}
>
{tiles.map((tile, index) => {
const p = tween(m.frame, m.fps, {
from: m.delay + delayFor(index, tiles.length),
duration: m.enterFrames,
motion: m.preset,
});
const fill = tile.accent ? (accentColor ?? theme.colors.accent) : (background ?? theme.colors.surface);
return (
<div
key={index}
style={{
gridColumn: `span ${spans[index]}`,
display: "flex",
flexDirection: "column",
justifyContent: "flex-end",
gap: u(10),
padding: u(34),
borderRadius: u(theme.radius),
background: fill,
border: `1px solid ${tile.accent ? fill : (borderColor ?? theme.colors.border)}`,
...effectStyle(effect, p, u(40)),
}}
>
<div
style={{
fontFamily: theme.fonts.heading,
fontWeight: theme.headingWeight,
fontSize: u(40),
lineHeight: 1.1,
letterSpacing: "-0.02em",
color: tile.accent
? (accentForeground ?? theme.colors.accentForeground)
: (color ?? theme.colors.foreground),
}}
>
{tile.title}
</div>
{tile.body && (
<div
style={{
fontSize: u(26),
lineHeight: 1.35,
color: tile.accent
? alpha(accentForeground ?? theme.colors.accentForeground, 0.78)
: (mutedColor ?? theme.colors.muted),
}}
>
{tile.body}
</div>
)}
</div>
);
})}
</div>
);
}