5 years in the past I revealed an article on methods to create a responsive grid of hexagon shapes. It was the one method that didn’t require media queries or JavaScript. It really works with any variety of gadgets, permitting you to simply management the scale and hole utilizing CSS variables.
I’m utilizing float, inline-block, setting font-size equal to 0, and so on. In 2026, this will likely sound a bit hacky and outdated. Probably not since this methodology works wonderful and is properly supported, however can we do higher utilizing trendy options? In 5 years, many issues have modified and we are able to enhance the above implementation and make it much less hacky!
Assist is proscribed to Chrome solely as a result of this system makes use of lately launched options, together with corner-shape, sibling-index(), and unit division.
The CSS code is shorter and accommodates fewer magic numbers than the final time I approached this. Additionally, you will discover some advanced calculations that we are going to dissect collectively.
Earlier than diving into this new demo, I extremely advocate studying my earlier article first. It’s not necessary, nevertheless it permits you to evaluate each strategies and understand how a lot (and quickly) CSS has developed within the final 5 years by introducing new options that make one-difficult issues like this simpler.
The Hexagon Form
Let’s begin with the hexagon form, which is the primary aspect of our grid. Beforehand, I needed to depend on clip-path: polygon() to create it:
.hexagon {
--s: 100px;
width: var(--s);
top: calc(var(--s) * 1.1547);
clip-path: polygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%);
}
However now, we are able to depend on the brand new corner-shape property which works alongside the border-radius property:
.hexagon {
width: 100px;
aspect-ratio: cos(30deg);
border-radius: 50% / 25%;
corner-shape: bevel;
}
Easier than how we used to bevel components, and as a bonus, we are able to add a border to the form with out workarounds!
The corner-shape property is the primary trendy function we’re counting on. It makes drawing CSS shapes loads simpler than conventional strategies, like utilizing clip-path. You possibly can nonetheless preserve utilizing the clip-path methodology, in fact, for higher assist (and for those who don’t want a border on the aspect), however here’s a extra trendy implementation:
.hexagon {
width: 100px;
aspect-ratio: cos(30deg);
clip-path: polygon(-50% 50%,50% 100%,150% 50%,50% 0);
}
There are fewer factors contained in the polygon, and we changed the magic quantity 1.1547 with an aspect-ratio declaration. I gained’t spend extra time on the code of the shapes, however listed here are two articles I wrote in order for you an in depth clarification with extra examples:
The Responsive Grid
Now that we’ve got our form, let’s create the grid. It’s known as a “grid,” however I’m going to make use of a flexbox configuration:
.container {
--s: 120px; /* dimension */
--g: 10px; /* hole */
show: flex;
hole: var(--g);
flex-wrap: wrap;
}
.container > * {
width: var(--s);
aspect-ratio: cos(30deg);
border-radius: 50% / 25%;
corner-shape: bevel;
}
Nothing fancy to date. From there, we add a backside margin to all gadgets to create an overlap between the rows:
.container > * {
margin-bottom: calc(var(--s)/(-4*cos(30deg)));
}
The final step is so as to add a left margin to the primary merchandise of the even rows (i.e., 2nd, 4th, sixth, and so). This margin will create the shift between rows to attain an ideal grid.
Mentioned like that, it sounds simple, nevertheless it’s the trickiest half the place we want advanced calculations. The grid is responsive, so the “first” merchandise we're in search of will be any merchandise relying, on the container dimension, merchandise dimension, hole, and so on.
Let’s begin with a determine:

Our grid can have two features relying on the responsiveness. We will both have the identical variety of gadgets in all of the rows (Grid 1 within the determine above) or a distinction of 1 merchandise between two consecutive rows (Grid 2). The N and M variables signify the variety of gadgets within the rows. In Grid 1 we've got N = M, and in Grid 2 we've got M = N - 1.
In Grid 1, the gadgets with a left margin are 6, 16, 26, and so on., and in Grid 2, they're 7, 18, 29, and so on. Let’s attempt to establish the logic behind these numbers.
The primary merchandise in each grids (6 or 7) is the primary one within the second row, so it’s the merchandise N + 1. The second merchandise (16 or 18) is the primary one within the third row, so it’s the merchandise N + M + N + 1. The third merchandise (26 or 29) is the merchandise N + M + N + M + N + 1. If you happen to look carefully, you may see a sample that we are able to specific utilizing the next components:
N*i + M*(i - 1) + 1
…the place i is a constructive integer (zero excluded). The gadgets we're in search of will be discovered utilizing the next pseudo-code:
for(i = 0; i< ?? ;i++) {
index = N*i + M*(i - 1) + 1
Add margin to gadgets[index]
}
We don’t have loops in CSS, although, so we must do one thing completely different. We will acquire the index of every merchandise utilizing the brand new sibling-index() operate. The logic is to check if that index respect the earlier components.
As an alternative of scripting this:
index = N*i + M*(i - 1) + 1
…let’s specific i utilizing the index:
i = (index - 1 + M)/(N + M)
We all know that i is a constructive integer (zero excluded), so for every merchandise, we get its index and take a look at if (index - 1 + M)/(N + M) is a constructive integer. Earlier than that, let’s calculate the variety of gadgets, N and M.
Calculating the variety of gadgets per row is similar as calculating what number of gadgets can slot in that row.
N = spherical(down,container_size / item_size);
Dividing the container dimension by the merchandise dimension offers us a quantity. If we spherical()` it all the way down to the closest integer, we get the variety of gadgets per row. However we've got a niche between gadgets, so we have to account for this within the components:
N = spherical(down, (container_size + hole)/ (item_size + hole));
We do the identical for M, however this time we have to additionally account for the left margin utilized to the primary merchandise of the row:
M = spherical(down, (container_size + hole - margin_left)/ (item_size + hole));
Let’s take a better look and establish the worth of that margin within the subsequent determine:

It’s equal to half the scale of an merchandise, plus half the hole:
M = spherical(down, (container_size + hole - (item_size + hole)/2)/(item_size + hole));
M = spherical(down, (container_size - (item_size - hole)/2)/(item_size + hole));
The merchandise dimension and the hole are outlined utilizing the --s and --g variables, however what in regards to the container dimension? We will depend on container question models and use 100cqw.
Let’s write what we've got till now utilizing CSS:
.container {
--s: 120px; /* dimension */
--g: 10px; /* hole */
container-type: inline-size; /* we make it a container to make use of 100cqw */
}
.container > * {
--_n: spherical(down,(100cqw + var(--g))/(var(--s) + var(--g)));
--_m: spherical(down,(100cqw - (var(--s) - var(--g))/2)/(var(--s) + var(--g)));
--_i: calc((sibling-index() - 1 + var(--_m))/(var(--_n) + var(--_m)));
margin-left: ???; /* We're getting there! */
}
We will use mod(var(--_i),1) to check if --_i is an integer. If it’s an integer, the consequence is the same as 0. In any other case, it’s equal to a worth between 0 and 1.
We will introduce one other variable and use the brand new if() operate!
.container {
--s: 120px; /* dimension */
--g: 10px; /* hole */
container-type: inline-size; /* we make it a container to make use of 100cqw */
}
.container > * {
--_n: spherical(down,(100cqw + var(--g))/(var(--s) + var(--g)));
--_m: spherical(down,(100cqw - (var(--s) - var(--g))/2)/(var(--s) + var(--g)));
--_i: calc((sibling-index() - 1 + var(--_m))/(var(--_n) + var(--_m)));
--_c: mod(var(--_i),1);
margin-left: if(type(--_c: 0) calc((var(--s) + var(--g))/2) else 0;);
}
Tada!
It’s necessary to notice that it's essential to register the variable --_c variable utilizing @property to have the ability to do the comparability (I write extra about this in “Tips on how to appropriately use if()in CSS”).
It is a good use case for if(), however we are able to do it in another way:
--_c: spherical(down, 1 - mod(var(--_i), 1));
The mod() operate offers us a worth between 0 and 1, the place 0 is the worth we would like. -1*mod() offers us a worth between -1 and 0. 1 - mod() offers us a worth between 0 and 1, however this time it’s the 1 we want. We apply spherical() to the calculation, and the consequence can be both 0 or 1. The --_c variable is now a Boolean variable that we are able to use instantly inside a calculation.
margin-left: calc(var(--_c) * (var(--s) + var(--g))/2);
If --_c is the same as 1, we get a margin. In any other case, the margin is the same as 0. This time you don’t must register the variable utilizing @property. I personally choose this methodology because it requires much less code, however the if() methodology can also be fascinating.
Ought to I keep in mind all these formulation by coronary heart?! It’s an excessive amount of!
No, you don’t. I attempted to offer an in depth clarification behind the maths, nevertheless it’s not necessary to grasp it to work with the grid. All you must do is replace the variables that management the scale and hole. No want to the touch the half that set the left margin. We are going to even discover how the identical code construction can work with extra shapes!
Extra Examples
The widespread use case is a hexagon form however what about different shapes? We will, for instance, contemplate a rhombus and, for this, we merely modify the code that controls the form.
From this:
.container > * {
aspect-ratio: cos(30deg);
border-radius: 50% / 25%;
corner-shape: bevel;
margin-bottom: calc(var(--s)/(-4*cos(30deg)));
}
…to this:
.container > * {
aspect-ratio: 1;
border-radius: 50%;
corner-shape: bevel;
margin-bottom: calc(var(--s)/-2);
}
A responsive grid of rhombus shapes — with no effort! Let’s attempt an octagon:
.container > * {
aspect-ratio: 1;
border-radius: calc(100%/(2 + sqrt(2)));
corner-shape: bevel;
margin-bottom: calc(var(--s)/(-1*(2 + sqrt(2))));
}
Nearly! For an octagon, we have to modify the hole as a result of we want extra horizontal house between the gadgets:
.container {
--g: calc(10px + var(--s)/(sqrt(2) + 1));
hole: 10px var(--g);
}
The variable --g features a portion of the scale var(--s)/(sqrt(2) + 1) and is utilized as a row hole, whereas the column hole is saved the identical (10px).
From there, we are able to additionally get one other sort of hexagon grid:
And why not a grid of circles as properly? Right here we go:
As you may see, we didn’t contact the advanced calculation that units the left margin in any of these examples. All we needed to do was to play with the border-radius and aspect-ratio properties to regulate the form and modify the underside margin to rectify the overlap. In some circumstances, we have to modify the horizontal hole.
Conclusion
I'll finish this text with one other demo that may function a small homework for you:
This time, the shift is utilized to the odd rows moderately than the even ones. I allow you to dissect the code as a small train. Attempt to establish the change I've made and what’s the logic behind it (Trace: attempt to redo the calculation steps utilizing this new configuration.)









