bentoGrid
A Core block/component from Magic UI — clean-room reimplemented for Domphy (see methodology). Call bentoGrid() with no arguments for a working demo, or edit the code below live.
Implementation notes
Generic 'dumb' grid shell (CSS grid, gridAutoFlow: dense, caller-supplied columnSpan/rowSpan per card) with hover choreography (background zoom+blur via a data-attribute selector, CTA arrow nudge, link() patch handles the underline-on-hover text state). The background slot accepts arbitrary DomphyElement content per the spec's 'pluggable background widget' guidance. Default demo cards use a simple original drifting gradient-blob background (CSS keyframe + radial-gradient) rather than reimplementing the specific rich widgets shown in the reference demo (file carousel, animated notification list, calendar, globe/beam graphic) — those are called out in the spec itself as swappable per-card content, not part of BentoGrid/BentoCard's own required behavior, so building the shell generically and leaving richer backgrounds to callers (or to the sibling 'effects'/'device-mocks' category components other agents in this pipeline are implementing) was the intentional scope boundary here.
Status: ported · Reference: Magic UI original
// magicui "BentoGrid" — clean-room reimplementation from the public
// behavior/visual spec only (no upstream source viewed or copied). A
// responsive CSS-grid mosaic of unevenly sized feature cards. The grid/card
// shell is intentionally "dumb" layout plumbing — callers plug arbitrary
// decorative content into each card's `background` slot; the shell itself
// only supplies the mosaic layout, the text/CTA layer, and the shared hover
// choreography (background zoom, CTA underline + arrow nudge).
import type { DomphyElement, Listener, StyleObject } from "@domphy/core";
import { hashString } from "@domphy/core";
import { heading, link, paragraph } from "@domphy/ui";
import { type ThemeColor, themeColor, themeSpacing } from "@domphy/theme";
export interface BentoCardSpec {
title: string;
description: string;
href?: string;
ctaLabel?: string;
/** Small icon/graphic rendered above the title. */
icon?: DomphyElement;
/** Grid-column span for this card (in `columns` units). Caller decides the mosaic pattern. */
columnSpan?: number;
/** Grid-row span for this card. */
rowSpan?: number;
/** Arbitrary decorative content rendered behind the text layer (carousel, animated list, beam graphic, …). */
background?: DomphyElement;
}
export interface BentoGridProps {
cards?: BentoCardSpec[];
/** Number of grid columns at the widest breakpoint. Defaults to 3. */
columns?: number;
style?: StyleObject;
}
// ---------------------------------------------------------------------------
// Hand-authored generic line icons (24x24, stroke=currentColor) — simple
// geometric silhouettes, not sourced from any icon library.
// ---------------------------------------------------------------------------
function lineIcon(children: DomphyElement[]): DomphyElement<"span"> {
return {
span: [
{
svg: children,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round",
role: "img",
ariaHidden: "true",
style: { width: "100%", height: "100%" },
} as DomphyElement<"svg">,
],
ariaHidden: "true",
style: { display: "inline-flex", width: themeSpacing(8), height: themeSpacing(8) },
};
}
const boltIcon = () => lineIcon([{ polyline: null, points: "13,3 4,14 12,14 11,21 20,10 12,10" }]);
const syncIcon = () => lineIcon([
{ path: null, d: "M4 11a8 8 0 0 1 14-5" },
{ polyline: null, points: "18,3 18,7 14,7" },
{ path: null, d: "M20 13a8 8 0 0 1-14 5" },
{ polyline: null, points: "6,21 6,17 10,17" },
]);
const globeIcon = () => lineIcon([
{ circle: null, cx: "12", cy: "12", r: "9" },
{ ellipse: null, cx: "12", cy: "12", rx: "4", ry: "9" },
{ line: null, x1: "3", y1: "12", x2: "21", y2: "12" },
]);
const chartIcon = () => lineIcon([
{ line: null, x1: "4", y1: "20", x2: "4", y2: "10" },
{ line: null, x1: "12", y1: "20", x2: "12", y2: "4" },
{ line: null, x1: "20", y1: "20", x2: "20", y2: "14" },
]);
const shieldIcon = () => lineIcon([
{ path: null, d: "M12 3l7 3v6c0 4.5-3 7.5-7 9-4-1.5-7-4.5-7-9V6z" },
{ polyline: null, points: "9,12 11,14 15,10" },
]);
const arrowRightIcon = () => ({
span: [
{
svg: [
{ line: null, x1: "4", y1: "12", x2: "18", y2: "12" },
{ polyline: null, points: "12,6 18,12 12,18" },
],
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round",
role: "img",
ariaHidden: "true",
style: { width: "100%", height: "100%" },
} as DomphyElement<"svg">,
],
ariaHidden: "true",
dataBentoArrow: "true",
style: {
display: "inline-flex",
width: themeSpacing(4),
height: themeSpacing(4),
transition: "transform 200ms ease",
},
}) as DomphyElement<"span">;
/** Soft drifting gradient blob — the default, generic "pluggable background widget". */
function gradientBlob(color: ThemeColor): DomphyElement<"div"> {
const keyframes = {
"0%,100%": { transform: "translate(-8%, -8%) scale(1)" },
"50%": { transform: "translate(8%, 8%) scale(1.15)" },
};
const animationName = `bento-blob-${hashString(JSON.stringify(keyframes) + color)}`;
// `_doctorDisable` is a doctor-only annotation not present in core's strict
// `PartialElement` type — build through an untyped literal, then assert, so
// the excess-property check doesn't fire (mirrors verticalDivider() in the
// shadcn sidebar family).
const element = {
div: null,
ariaHidden: "true",
// Decorative gradient blob with no text of its own — exempt from the
// missing-color contract.
_doctorDisable: "missing-color",
style: {
position: "absolute",
inset: "-25%",
borderRadius: "50%",
background: (listener: Listener) =>
`radial-gradient(circle at 30% 30%, ${themeColor(listener, "shift-9", color)}, transparent 60%)`,
opacity: 0.35,
filter: "blur(28px)",
animation: `${animationName} 9s ease-in-out infinite`,
[`@keyframes ${animationName}`]: keyframes,
},
};
return element as DomphyElement<"div">;
}
const DEFAULT_CARDS: BentoCardSpec[] = [
{
title: "Ship faster",
description: "A component library and a design system that stay in lockstep, so nothing drifts.",
href: "#",
icon: boltIcon(),
background: gradientBlob("primary"),
columnSpan: 2,
rowSpan: 2,
},
{
title: "Stay in sync",
description: "Every change propagates instantly across your team's workspace.",
href: "#",
icon: syncIcon(),
background: gradientBlob("secondary"),
},
{
title: "Global by default",
description: "Edge-rendered everywhere, with locale-aware content out of the box.",
href: "#",
icon: globeIcon(),
background: gradientBlob("info"),
rowSpan: 2,
},
{
title: "Built-in analytics",
description: "Understand usage without wiring up a separate dashboard.",
href: "#",
icon: chartIcon(),
background: gradientBlob("success"),
columnSpan: 2,
},
{
title: "Enterprise-grade security",
description: "Audited, encrypted, and access-controlled from day one.",
href: "#",
icon: shieldIcon(),
background: gradientBlob("attention"),
},
];
/** One mosaic tile: decorative background layer, edge-fade scrim, then the text/CTA layer. */
function bentoCard(card: BentoCardSpec): DomphyElement<"div"> {
const ctaLabel = card.ctaLabel ?? "Learn more";
const backgroundLayer: DomphyElement<"div"> | null = card.background
? {
div: [card.background],
ariaHidden: "true",
dataBentoBackground: "true",
style: {
position: "absolute",
inset: 0,
overflow: "hidden",
pointerEvents: "none",
transition: "transform 400ms ease, filter 400ms ease",
},
}
: null;
// `_doctorDisable` is a doctor-only annotation not present in core's strict
// `PartialElement` type — build through an untyped literal, then assert, so
// the excess-property check doesn't fire (mirrors verticalDivider() in the
// shadcn sidebar family).
const scrimElement = {
div: null,
ariaHidden: "true",
// Decorative gradient scrim with no text of its own — exempt from the
// missing-color contract.
_doctorDisable: "missing-color",
style: {
position: "absolute",
inset: 0,
pointerEvents: "none",
background: (listener: Listener) =>
`linear-gradient(to top, ${themeColor(listener, "inherit")} 20%, transparent 70%)`,
},
};
const scrim = scrimElement as DomphyElement<"div">;
const content: DomphyElement<"div"> = {
div: [
...(card.icon
? [
{
span: [card.icon],
style: { display: "inline-flex", color: (listener: Listener) => themeColor(listener, "shift-10") },
} as DomphyElement<"span">,
]
: []),
{ h3: card.title, $: [heading({ color: "neutral" })] },
{ p: card.description, $: [paragraph({ color: "neutral" })] },
{
a: [ctaLabel, arrowRightIcon()],
href: card.href ?? "#",
style: {
display: "inline-flex",
alignItems: "center",
gap: themeSpacing(1),
marginTop: "auto",
},
$: [link({ color: "primary" })],
},
],
style: {
position: "relative",
display: "flex",
flexDirection: "column",
gap: themeSpacing(2),
height: "100%",
padding: themeSpacing(5),
justifyContent: "flex-end",
},
};
return {
div: [...(backgroundLayer ? [backgroundLayer, scrim] : []), content],
_key: card.title,
dataTone: "shift-1",
style: {
position: "relative",
overflow: "hidden",
borderRadius: themeSpacing(4),
gridColumn: card.columnSpan ? `span ${card.columnSpan}` : undefined,
gridRow: card.rowSpan ? `span ${card.rowSpan}` : undefined,
backgroundColor: (listener: Listener) => themeColor(listener, "inherit"),
color: (listener: Listener) => themeColor(listener, "shift-9"),
outline: (listener: Listener) => `1px solid ${themeColor(listener, "shift-3")}`,
outlineOffset: "-1px",
transition: "outline-color 200ms ease",
"&:hover": {
outline: (listener: Listener) => `1px solid ${themeColor(listener, "shift-6")}`,
outlineOffset: "-1px",
},
"&:hover [data-bento-background]": { transform: "scale(1.08)", filter: "blur(20px)" },
"&:hover [data-bento-arrow]": { transform: `translateX(${themeSpacing(1)})` },
},
};
}
/**
* Responsive "bento box" mosaic of feature cards. Call with no arguments for
* a working demo — five cards with a mix of column/row spans, each with a
* drifting gradient-blob background.
*/
function bentoGrid(props: BentoGridProps = {}): DomphyElement<"div"> {
const columns = props.columns ?? 3;
const cards = props.cards ?? DEFAULT_CARDS;
return {
div: cards.map((card) => bentoCard(card)),
style: {
display: "grid",
gridTemplateColumns: "repeat(1, 1fr)",
gridAutoFlow: "dense",
gridAutoRows: themeSpacing(44),
gap: themeSpacing(4),
"@media (min-width: 48em)": {
gridTemplateColumns: `repeat(${columns}, 1fr)`,
},
...(props.style ?? {}),
},
};
}
export { bentoGrid };