A minimal bento layout primitive with span props, a 40ms blur-rise reveal stagger on first view, and hover isolation that dims sibling cells.
npx shadcn@latest add @paragon/bento-grid"use client";
import * as React from "react";
import { motion, useReducedMotion, type Variants } from "motion/react";
import { cn } from "@/lib/utils";
const containerVariants: Variants = {
hidden: {},
visible: { transition: { staggerChildren: 0.04 } },
};
export interface BentoGridProps
extends React.ComponentProps<typeof motion.div> {}
/**
* The bento layout container: a 3-column grid (1 column on small screens)
* whose cells reveal on first view with a 40ms blur-rise stagger. On fine
* pointers, hovering a cell dims its siblings to 0.7 — a CSS transition,
* so fast pointer moves retarget instead of flickering.
*/
export function BentoGrid({ className, ...props }: BentoGridProps) {
return (
<motion.div
data-slot="bento-grid"
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "0px 0px -48px 0px" }}
variants={containerVariants}
className={cn(
"group/bento grid grid-cols-1 gap-4 sm:grid-cols-3",
className,
)}
{...props}
/>
);
}
const colSpanClasses = {
1: "sm:col-span-1",
2: "sm:col-span-2",
3: "sm:col-span-3",
} as const;
const rowSpanClasses = {
1: "sm:row-span-1",
2: "sm:row-span-2",
} as const;
export interface BentoCellProps
extends React.ComponentProps<typeof motion.div> {
/** Columns to span at the `sm` breakpoint and up. */
colSpan?: 1 | 2 | 3;
/** Rows to span at the `sm` breakpoint and up. */
rowSpan?: 1 | 2;
}
/**
* One bento cell: a plain shadow-border card surface with span props.
* Deliberately minimal — bring your own content; no baked-in effects.
*
* Structure note: the hover-dim lives on an outer wrapper while the reveal
* animation runs on the inner surface — motion leaves an inline `opacity`
* on whatever it animates, which would otherwise override the dim class.
* Variants still propagate from BentoGrid through the plain wrapper.
*/
export function BentoCell({
colSpan = 1,
rowSpan = 1,
className,
...props
}: BentoCellProps) {
const reducedMotion = useReducedMotion();
const cellVariants: Variants = React.useMemo(
() => ({
hidden: reducedMotion
? { opacity: 0 }
: { opacity: 0, y: 12, filter: "blur(4px)" },
visible: {
opacity: 1,
y: 0,
filter: "blur(0px)",
transition: { type: "spring", duration: 0.4, bounce: 0 },
},
}),
[reducedMotion],
);
return (
<div
data-slot="bento-cell"
data-bento-cell=""
className={cn(
colSpanClasses[colSpan],
rowSpanClasses[rowSpan],
"transition-opacity duration-(--duration-quick) ease-out",
"[@media(hover:hover)_and_(pointer:fine)]:group-has-[[data-bento-cell]:hover]/bento:[&:not(:hover)]:opacity-70",
)}
>
<motion.div
variants={cellVariants}
className={cn(
"h-full rounded-xl bg-card p-5 text-card-foreground shadow-border",
className,
)}
{...props}
/>
</div>
);
}