Simple bento box layout with CSS grid
This quick post summarizes how I solved the problem of creating a psuedo-random bento-box layout for my website.
The "bento box" layout has become a popular design trend for showcasing a variety of content in a visually interesting grid. I wanted to implement something similar for my own website to display a list of blog posts, but I didn't want a static, repetitive layout. I wanted it to feel a bit more dynamic and pseudo-random. This post walks through how I achieved this using CSS Grid and a little bit of Javascript.
The Foundation: CSS Grid
The foundation of the layout is a simple CSS grid container. Using Tailwind CSS, I can define a 4-column grid that adapts to a single column on smaller screens:
<div className="max-w-6xl mx-4 lg:mx-auto my-16 grid grid-cols-1 md:grid-cols-4 gap-4">
{/* ... cards go here */}
</div>
This gives us a responsive grid that works well across different screen sizes.
Creating Pseudo-Random Patterns
To avoid a monotonous layout, I defined several column-span patterns that cards could follow. Each pattern represents how many columns each card in a row should span:
const patterns = [
[2, 2], // Two cards, each spanning 2 columns
[1, 2, 1], // Three cards: 1 column, 2 columns, 1 column
[1, 1, 1, 1], // Four cards, each spanning 1 column
[2, 1, 1], // Three cards: 2 columns, 1 column, 1 column
[1, 1, 2], // Three cards: 1 column, 1 column, 2 columns
];
All patterns add up to 4 columns total, ensuring they fit perfectly within our grid.
The Pattern Selection Logic
Here's the function that handles pattern selection and ensures we don't get consecutive identical rows:
function getRandomPattern(currentPattern) {
const patterns = [[2, 2], [1, 2, 1], [1, 1, 1, 1], [2, 1, 1], [1, 1, 2]];
const index = Math.round(Math.random() * (patterns.length - 1));
let newPattern = patterns[index];
// Avoid consecutive identical patterns
if (currentPattern && arraysEqual(newPattern, currentPattern)) {
newPattern = patterns[(index + 1) % patterns.length];
}
return newPattern;
}
This function randomly selects a pattern, but if it's the same as the current pattern, it picks the next one in the array. This prevents visual monotony while maintaining the pseudo-random feel.
Putting It All Together
The complete implementation maps through the blog posts and applies the patterns dynamically:
let currentPattern = getRandomPattern();
let position = -1;
return (
<div className="max-w-6xl mx-4 lg:mx-auto my-16 grid grid-cols-1 md:grid-cols-4 lg:grid-cols-4 gap-4">
{posts.map((post, index) => {
const span = currentPattern[++position];
// When we reach the end of the current pattern, get a new one
if (position === currentPattern.length - 1) {
currentPattern = getRandomPattern(currentPattern);
position = -1;
}
return <InteractiveCard key={index} {...post} span={span} />;
})}
</div>
);
The InteractiveCard Component
Each card receives a span
prop that determines how many columns it should occupy:
function InteractiveCard({ span, ...post }) {
return (
<div className={`col-span-${span} ...`} >
{/* Card content */}
</div>
);
}
Helper Function
I was running into some issues with comparing arrays, so made this utility function to compare element values:
function arraysEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false;
}
return true;
}
The Result
This approach creates a visually interesting bento box layout that feels organic and varied while maintaining structural consistency. Each row uses a different pattern from the previous one, and the pseudo-random selection ensures that even with a limited set of patterns, the layout never feels predictable.
What I like about this solution is its simplicity - no libraries required. Just CSS Grid, a few predefined patterns, and some basic JavaScript logic to tie it all together.

Potential Enhancements
A few ideas for taking this further:
- Make it generic for any number of columns
- Implement different patterns for different screen sizes
- Spanning rows as well as columns
- Consider content-aware spanning (longer titles get wider cards)
Jeremy Atkinson
Jeremy is a structural engineer, researcher, and developer from BC. He works on Calcs.app and writes at Kinson.io