Original by Plinth ยท MIT
Cards
Bento Grid
A named-area bento layout whose tiles lift and catch a brass wash under the pointer.
Install
npx shadcn@latest add https://plinthui.com/r/bento-grid.json'use client';
const css = `
.bento {
display: grid;
/* Named areas rather than span counts: the shape of the layout is legible in
the CSS, and rearranging it is editing a picture. */
grid-template-areas:
"a a b"
"c d b";
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-auto-rows: minmax(0, 1fr);
gap: 10px;
width: min(100%, 560px);
aspect-ratio: 16 / 9;
}
.bento__cell {
position: relative;
display: flex;
flex-direction: column;
gap: 5px;
overflow: hidden;
padding: 14px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.09);
background: rgba(255, 255, 255, 0.035);
transition: border-color 240ms ease, background-color 240ms ease, transform 240ms ease;
}
.bento__cell:nth-child(1) { grid-area: a; }
.bento__cell:nth-child(2) { grid-area: b; }
.bento__cell:nth-child(3) { grid-area: c; }
.bento__cell:nth-child(4) { grid-area: d; }
/* A brass wash following the pointer, on a pseudo-element so it can fade
independently of the cell's own background. */
.bento__cell::before {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(200px circle at var(--mx, 50%) var(--my, 50%), rgba(255, 202, 122, 0.16), transparent 62%);
opacity: 0;
transition: opacity 240ms ease;
pointer-events: none;
}
.bento__cell:hover {
border-color: rgba(255, 202, 122, 0.4);
background: rgba(255, 255, 255, 0.06);
transform: translateY(-2px);
}
.bento__cell:hover::before { opacity: 1; }
.bento__tag {
font: 500 10px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
letter-spacing: 0.14em;
color: rgba(255, 202, 122, 0.72);
}
.bento__title {
margin: 0;
font: 600 clamp(13px, 2.6vw, 16px)/1.25 ui-sans-serif, system-ui, sans-serif;
letter-spacing: -0.01em;
color: #f3f0e9;
}
.bento__body {
margin: 0;
font: 400 clamp(11px, 2.1vw, 12.5px)/1.45 ui-sans-serif, system-ui, sans-serif;
color: rgba(243, 240, 233, 0.55);
}
@media (prefers-reduced-motion: reduce) {
.bento__cell { transition: border-color 240ms ease, background-color 240ms ease; }
.bento__cell:hover { transform: none; }
}
`;
export interface BentoCell {
tag: string;
title: string;
body?: string;
}
const CELLS: BentoCell[] = [
{ tag: '001', title: 'Smart actions', body: 'Refunds, plan changes and CRM updates, handled without a handoff.' },
{ tag: '002', title: 'Auto-resolve', body: 'Deflects the routine and closes the rest inside guardrails you set.' },
{ tag: '003', title: 'Assist' },
{ tag: '004', title: 'Recall' },
];
export function BentoGrid({ cells = CELLS }: { cells?: BentoCell[] }) {
return (
<>
<style>{css}</style>
<div
className="bento"
// Delegated from the grid rather than bound per cell: one listener, and
// adding cells needs no extra wiring.
onPointerMove={event => {
const cell = (event.target as HTMLElement).closest<HTMLElement>('.bento__cell');
if (!cell) return;
const r = cell.getBoundingClientRect();
cell.style.setProperty('--mx', `${event.clientX - r.left}px`);
cell.style.setProperty('--my', `${event.clientY - r.top}px`);
}}
>
{cells.map(cell => (
<article className="bento__cell" key={cell.tag}>
<span className="bento__tag">{cell.tag}</span>
<h3 className="bento__title">{cell.title}</h3>
{cell.body ? <p className="bento__body">{cell.body}</p> : null}
</article>
))}
</div>
</>
);
}