Bento
import { useEffect, useState } from 'react';
const css = `
.card-bento { position: relative; width: 236px; height: 176px; }
.card-bento__tile {
position: absolute;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 10px;
border-radius: 14px;
background: light-dark(#f3f4f7, #1a1c1f);
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
overflow: hidden;
transition:
left 0.65s cubic-bezier(0.65, 0, 0.35, 1),
top 0.65s cubic-bezier(0.65, 0, 0.35, 1),
width 0.65s cubic-bezier(0.65, 0, 0.35, 1),
height 0.65s cubic-bezier(0.65, 0, 0.35, 1);
}
.card-bento__tile small { color: light-dark(#5f626a, #a7abb2); font-size: 10px; }
.card-bento__tile b { color: light-dark(#373a40, #f1f2f3); font-size: 16px; font-weight: 600; letter-spacing: -0.01em; white-space: nowrap; }
.card-bento__tile--hero { background: linear-gradient(135deg, light-dark(#cbced3, #393c41), light-dark(#ebedf0, #232529)); }
.card-bento__tile--hero small { color: light-dark(#42454c, #e5e6e9); }
@media (prefers-reduced-motion: reduce) { .card-bento__tile { transition: none; } }
`;
// Layouts on a 3 x 3 grid as [col, row, colSpan, rowSpan] per tile, in tile order.
const LAYOUTS: [number, number, number, number][][] = [
[[0, 0, 2, 2], [2, 0, 1, 1], [2, 1, 1, 2], [0, 2, 2, 1]],
[[1, 0, 2, 2], [0, 0, 1, 2], [0, 2, 1, 1], [1, 2, 2, 1]],
[[0, 1, 2, 2], [0, 0, 2, 1], [2, 0, 1, 2], [2, 2, 1, 1]],
[[1, 1, 2, 2], [0, 2, 1, 1], [0, 0, 1, 2], [1, 0, 2, 1]],
];
const TILES = [
{ k: 'Revenue', v: '$48.2k', hero: true },
{ k: 'Users', v: '9.1k' },
{ k: 'Uptime', v: '99.98%' },
{ k: 'Response', v: '182 ms' },
];
const GAP = 6;
const CW = (236 - GAP * 2) / 3;
const CH = (176 - GAP * 2) / 3;
export function BentoCard() {
const [n, setN] = useState(0);
useEffect(() => {
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const id = window.setInterval(() => setN((v) => (v + 1) % LAYOUTS.length), 2000);
return () => clearInterval(id);
}, []);
return (
<>
<style>
{css}
</style>
<div className="card-bento" role="group" aria-label="Dashboard summary">
{TILES.map((t, i) => {
const [c, r, cs, rs] = LAYOUTS[n][i];
return (
<div
key={t.k}
className={`card-bento__tile${t.hero ? ' card-bento__tile--hero' : ''}`}
style={{
left: c * (CW + GAP),
top: r * (CH + GAP),
width: cs * CW + (cs - 1) * GAP,
height: rs * CH + (rs - 1) * GAP,
}}
>
<small>{t.k}</small>
<b>{t.v}</b>
</div>
);
})}
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCardBento(el) on the mounted element instead.
<div class="card-bento" role="group" aria-label="Dashboard summary">
<div class="card-bento__tile card-bento__tile--hero" style="left:0;top:0;width:155.33333333333334px;height:115.33333333333333px">
<small>Revenue</small>
<b>$48.2k</b>
</div>
<div class="card-bento__tile" style="left:161.33333333333334px;top:0;width:74.66666666666667px;height:54.666666666666664px">
<small>Users</small>
<b>9.1k</b>
</div>
<div class="card-bento__tile" style="left:161.33333333333334px;top:60.666666666666664px;width:74.66666666666667px;height:115.33333333333333px">
<small>Uptime</small>
<b>99.98%</b>
</div>
<div class="card-bento__tile" style="left:0;top:121.33333333333333px;width:155.33333333333334px;height:54.666666666666664px">
<small>Response</small>
<b>182 ms</b>
</div>
</div>.card-bento { position: relative; width: 236px; height: 176px; }
.card-bento__tile {
position: absolute;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 10px;
border-radius: 14px;
background: light-dark(#f3f4f7, #1a1c1f);
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
overflow: hidden;
transition:
left 0.65s cubic-bezier(0.65, 0, 0.35, 1),
top 0.65s cubic-bezier(0.65, 0, 0.35, 1),
width 0.65s cubic-bezier(0.65, 0, 0.35, 1),
height 0.65s cubic-bezier(0.65, 0, 0.35, 1);
}
.card-bento__tile small { color: light-dark(#5f626a, #a7abb2); font-size: 10px; }
.card-bento__tile b { color: light-dark(#373a40, #f1f2f3); font-size: 16px; font-weight: 600; letter-spacing: -0.01em; white-space: nowrap; }
.card-bento__tile--hero { background: linear-gradient(135deg, light-dark(#cbced3, #393c41), light-dark(#ebedf0, #232529)); }
.card-bento__tile--hero small { color: light-dark(#42454c, #e5e6e9); }
@media (prefers-reduced-motion: reduce) { .card-bento__tile { transition: none; } }function initCardBento(root) {
var tiles = root.querySelectorAll('.card-bento__tile');
if (tiles.length !== 4) return;
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
// Layouts on a 3 x 3 grid as [col, row, colSpan, rowSpan] per tile, in tile order.
var LAYOUTS = [
[[0, 0, 2, 2], [2, 0, 1, 1], [2, 1, 1, 2], [0, 2, 2, 1]],
[[1, 0, 2, 2], [0, 0, 1, 2], [0, 2, 1, 1], [1, 2, 2, 1]],
[[0, 1, 2, 2], [0, 0, 2, 1], [2, 0, 1, 2], [2, 2, 1, 1]],
[[1, 1, 2, 2], [0, 2, 1, 1], [0, 0, 1, 2], [1, 0, 2, 1]],
];
var GAP = 6;
var CW = (236 - GAP * 2) / 3;
var CH = (176 - GAP * 2) / 3;
var n = 0;
var timer = setInterval(function () {
n = (n + 1) % LAYOUTS.length;
tiles.forEach(function (t, i) {
var l = LAYOUTS[n][i];
t.style.left = l[0] * (CW + GAP) + 'px';
t.style.top = l[1] * (CH + GAP) + 'px';
t.style.width = l[2] * CW + (l[2] - 1) * GAP + 'px';
t.style.height = l[3] * CH + (l[3] - 1) * GAP + 'px';
});
}, 2000);
return function () { clearInterval(timer); };
}
document.querySelectorAll('.card-bento').forEach(initCardBento);Goal: Create a "Bento" card.
Visual behavior: A bento-box dashboard whose four tiles reshuffle into a new layout every couple of seconds, each gliding and resizing to its new place without overlapping the others for long.
Technique:
- Four layouts on a 3 x 3 grid, each listing [col, row, colSpan, rowSpan] for every tile, all covering the grid exactly.
- Tiles are absolutely positioned; left, top, width and height are computed from the grid cell size and gap, and transition together on one ease-in-out curve.
- An interval moves to the next layout every 2s, so the tiles swap places and sizes.
Accessibility: A group labelled "Dashboard summary"; the tile text stays in the same reading order. Under prefers-reduced-motion it does not reshuffle.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Bento" card.
Visual behavior: A bento-box dashboard whose four tiles reshuffle into a new layout every couple of seconds, each gliding and resizing to its new place without overlapping the others for long.
Technique:
- Four layouts on a 3 x 3 grid, each listing [col, row, colSpan, rowSpan] for every tile, all covering the grid exactly.
- Tiles are absolutely positioned; left, top, width and height are computed from the grid cell size and gap, and transition together on one ease-in-out curve.
- An interval moves to the next layout every 2s, so the tiles swap places and sizes.
Accessibility: A group labelled "Dashboard summary"; the tile text stays in the same reading order. Under prefers-reduced-motion it does not reshuffle.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
A bento-box dashboard whose four tiles reshuffle into a new layout every couple of seconds, each gliding and resizing to its new place without overlapping the others for long.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- continuous, morph, card
- Techniques
- css transition, interval, state machine
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A group labelled "Dashboard summary"; the tile text stays in the same reading order. Under prefers-reduced-motion it does not reshuffle.