Skip to content
Plinth

Original by Plinth ยท MIT

Cards

Bento Grid

A named-area bento layout whose tiles lift and catch a brass wash under the pointer.

#card#bento#grid#layout#hover

Install

npx shadcn@latest add https://plinthui.com/r/bento-grid.json
Open in v0
'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>
    </>
  );
}

More in Cards

Similar tags