Friday, January 23, 2026

Responsive Hexagon Grid Utilizing Trendy CSS


5 years in the past I printed 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 objects, 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 nice and is effectively supported, however can we do higher utilizing fashionable options? In 5 years, many issues have modified and we will enhance the above implementation and make it much less hacky!

Assist is restricted to Chrome solely as a result of this method 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 complicated calculations that we’ll dissect collectively.

Earlier than diving into this new demo, I extremely advocate studying my earlier article first. It’s not obligatory, but it surely means that you can evaluate each strategies and notice 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 will 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 will add a border to the form with out workarounds!

The corner-shape property is the primary fashionable function we’re counting on. It makes drawing CSS shapes loads simpler than conventional strategies, like utilizing clip-path. You possibly can nonetheless hold utilizing the clip-path methodology, in fact, for higher help (and for those who don’t want a border on the aspect), however here’s a extra fashionable 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 received’t spend extra time on the code of the shapes, however listed here are two articles I wrote if you need an in depth rationalization with extra examples:

The Responsive Grid

Now that we have now 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 this point. From there, we add a backside margin to all objects 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 realize an ideal grid.

Stated like that, it sounds straightforward, but it surely’s the trickiest half the place we'd like complicated calculations. The grid is responsive, so the “first” merchandise we're searching for 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 facets relying on the responsiveness. We will both have the identical variety of objects 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 symbolize the variety of objects within the rows. In Grid 1 we have now N = M, and in Grid 2 we have now M = N - 1.

In Grid 1, the objects 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. Should you look carefully, you'll be able to see a sample that we will specific utilizing the next system:

N*i + M*(i - 1) + 1

…the place i is a optimistic integer (zero excluded). The objects we're searching for will be discovered utilizing the next pseudo-code:

for(i = 0; i< ?? ;i++) {
  index = N*i + M*(i - 1) + 1
  Add margin to objects[index]  
}

We don’t have loops in CSS, although, so we should do one thing completely different. We will receive the index of every merchandise utilizing the brand new sibling-index() operate. The logic is to check if that index respect the earlier system.

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 optimistic integer (zero excluded), so for every merchandise, we get its index and check if (index - 1 + M)/(N + M) is a optimistic integer. Earlier than that, let’s calculate the variety of objects, N and M.

Calculating the variety of objects per row is similar as calculating what number of objects can slot in that row.

N = spherical(down,container_size / item_size);

Dividing the container dimension by the merchandise dimension provides us a quantity. If we spherical()` it all the way down to the closest integer, we get the variety of objects per row. However we have now a niche between objects, so we have to account for this within the system:

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:

Illustrating the width of a single hexagon shape and the left margin between rows, which is one half the width of an item.

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 concerning the container dimension? We will depend on container question models and use 100cqw.

Let’s write what we have now 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 end result is the same as 0. In any other case, it’s equal to a price 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 vital to notice that you'll want to register the variable --_c variable utilizing @property to have the ability to do the comparability (I write extra about this in “How one can accurately use if()in CSS”).

It is a good use case for if(), however we will do it otherwise:

--_c: spherical(down, 1 - mod(var(--_i), 1));

The mod() operate provides us a price between 0 and 1, the place 0 is the worth we wish. -1*mod() provides us a price between -1 and 0. 1 - mod() provides us a price between 0 and 1, however this time it’s the 1 we'd like. We apply spherical() to the calculation, and the end result will likely be both 0 or 1. The --_c variable is now a Boolean variable that we will 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 favor this methodology because it requires much less code, however the if() methodology can 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 rationalization behind the mathematics, but it surely’s not obligatory to know it to work with the grid. All it's a must to 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 frequent use case is a hexagon form however what about different shapes? We will, for instance, think about a rhombus and, for this, we merely alter 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 strive 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))));
}

Virtually! For an octagon, we have to alter the hole as a result of we'd like extra horizontal area between the objects:

.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 will additionally get one other kind of hexagon grid:

And why not a grid of circles as effectively? Right here we go:

As you'll be able to see, we didn’t contact the complicated 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 manage the form and alter the underside margin to rectify the overlap. In some circumstances, we have to alter 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 relatively 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.)

Related Articles

Latest Articles