Thursday, February 12, 2026

Making a Responsive Pyramidal Grid With Trendy CSS


In the earlier article, we constructed the basic hexagon grid. It was a responsive implementation with out using media queries. The problem was to enhance a five-year outdated strategy utilizing trendy CSS.

Assist is proscribed to Chrome solely as a result of this system makes use of not too long ago launched options, together with corner-shape, sibling-index(), and unit division.

On this article, we’ll discover one other kind of grid: a pyramidal one. We’re nonetheless working with hexagon shapes, however a unique group of the weather.

A demo value a thousand phrases:

For higher visualization, open the full-page view of the demo to see the pyramidal construction. On display resize, you get a responsive habits the place the underside half begins to behave equally to the grid we created within the earlier article!

Cool proper? All of this was made and not using a single media question, JavaScript, or a ton of hacky CSS. You may chunk as many components as you need, and every thing will alter completely.

Earlier than we begin, do your self a favor and skim the earlier article should you haven’t already. I’ll skip just a few issues I’ve already defined there, comparable to how the shapes are created in addition to just a few formulation I’ll reuse right here. Just like the earlier article, the implementation of the pyramidal grid is an enchancment of a five-year outdated strategy, so if you wish to make a comparability between 2021 and 2026, try that older article as effectively.

The Preliminary Configuration

This time, we’ll depend on CSS Grid as an alternative of Flexbox. With this construction, it’s simple to regulate the position of things inside columns and rows slightly than adjusting margins.

.container {
  --s: 40px;  /* measurement  */
  --g: 5px;   /* hole */

  show: grid;
  grid-template-columns: repeat(auto-fit, var(--s) var(--s));
  justify-content: heart;
  hole: var(--g);
}

.container > * {
  grid-column-end: span 2;
  aspect-ratio: cos(30deg);
  border-radius: 50% / 25%;
  corner-shape: bevel;
  margin-bottom: calc((2*var(--s) + var(--g))/(-4*cos(30deg)));
}

I'm utilizing the basic repeated auto-fit to create as many columns because the free house permits. For the gadgets, it’s the identical code of the earlier article for creating hexagon shapes.

You wrote var(--s) twice. Is {that a} typo?

It’s not! I need my grid to all the time have a fair variety of columns, the place every merchandise spans two columns (that’s why I'm utilizing grid-column-end: span 2). With this configuration, I can simply management the shifting between the totally different rows.

Zooming into the gap between hexagon shapes, which are highlighted in pink.

Above is a screenshot of DevTools exhibiting the grid construction. If, for instance, merchandise 2 spans columns 3 and 4, then merchandise 4 ought to span columns 2 and three, merchandise 5 ought to span columns 4 and 5, and so forth.

It’s the identical logic with the responsive half. Every first merchandise of each different row is shifted by one column and begins on the second column.

Zooming into the gap between hexagon shapes, which are highlighted in pink.

With this configuration, the dimensions of an merchandise shall be equal to 2*var(--s) + var(--g). Because of this, the adverse backside margin is totally different from the earlier instance.

So, as an alternative of this:

margin-bottom: calc(var(--s)/(-4*cos(30deg)));

…I'm utilizing:

margin-bottom: calc((2*var(--s) + var(--g))/(-4*cos(30deg)));

Nothing fancy to date, however we have already got 80% of the code. Consider it or not, we're just one property away from finishing all the grid. All we have to do is ready the grid-column-start of some components to have the proper placement and, as you could have guessed, right here comes the trickiest half involving a fancy calculation.

The Pyramidal Grid

Let’s suppose the container is giant sufficient to include the pyramid with all the weather. In different phrases, we'll ignore the responsive half for now. Let’s analyze the construction and attempt to determine the patterns:

A stack of 28 hexagon shapes arranged in a pyramid-shaped grid. The first diagonal row on the right is highlighted showing how the shapes are aligned on the sides.

Whatever the variety of gadgets, the construction is by some means static. The gadgets on the left (i.e., the primary merchandise of every row) are all the time the identical (1, 2, 4, 7, 11, and so forth). A trivial answer is to focus on them utilizing the :nth-child() selector.

:nth-child(1) { grid-column-start: ?? }
:nth-child(2) { grid-column-start: ?? }
:nth-child(4) { grid-column-start: ?? }
:nth-child(7) { grid-column-start: ?? }
:nth-child(11) { grid-column-start: ?? }
/* and so on. */

The positions of all of them are linked. If merchandise 1 is positioned in column x, then merchandise 2 must be positioned in column x - 1, merchandise 4 in column x - 2, and so forth.

:nth-child(1) { grid-column-start: x - 0 } /* 0 will not be want however helpful to see the sample*/
:nth-child(2) { grid-column-start: x - 1 }
:nth-child(4) { grid-column-start: x - 2 }
:nth-child(7) { grid-column-start: x - 3 }
:nth-child(11) { grid-column-start: x - 4 }
/* and so on. */

Merchandise 1 is logically positioned within the center, so if our grid incorporates N columns, then x is the same as N/2:

:nth-child(1) { grid-column-start: N/2 - 0 }
:nth-child(2) { grid-column-start: N/2 - 1 }
:nth-child(4) { grid-column-start: N/2 - 2 }
:nth-child(7) { grid-column-start: N/2 - 3 }
:nth-child(11){ grid-column-start: N/2 - 4 }

And since every merchandise spans two columns, N/2 can be seen because the variety of gadgets that may match throughout the container. So, let’s replace our logic and think about N to be the variety of gadgets as an alternative of the variety of columns.

:nth-child(1) { grid-column-start: N - 0 }
:nth-child(2) { grid-column-start: N - 1 }
:nth-child(4) { grid-column-start: N - 2 }
:nth-child(7) { grid-column-start: N - 3 }
:nth-child(11){ grid-column-start: N - 4 }
/* and so on. */

To calculate the variety of gadgets, I'll use the identical system as within the earlier article:

N = spherical(down, (container_size + hole)/ (item_size + hole));

The one distinction is that the dimensions of an merchandise is now not var(--s)however 2*var(--s) + var(--g), which provides us the next CSS:

.container {
  --s: 40px;  /* measurement  */
  --g: 5px;   /* hole */

  container-type: inline-size; /* we make it a container to make use of 100cqw */
}

.container > * {
  --_n: spherical(down,(100cqw + var(--g))/(2*(var(--s) + var(--g))));
}

.container > *:nth-child(1) { grid-column-start: calc(var(--_n) - 0) }
.container > *:nth-child(2) { grid-column-start: calc(var(--_n) - 1) }
.container > *:nth-child(4) { grid-column-start: calc(var(--_n) - 2) }
.container > *:nth-child(7) { grid-column-start: calc(var(--_n) - 3) }
.container > *:nth-child(11){ grid-column-start: calc(var(--_n) - 4) }
/* and so on. */

It really works! We've got our pyramidal construction. It’s not but responsive, however we'll get there. By the way in which, in case your purpose is to construct such a construction with a hard and fast variety of gadgets, and also you don’t want responsive habits, then the above is ideal and also you’re carried out!

How come all of the gadgets are accurately positioned? We solely outlined the column for just a few gadgets, and we didn’t specify any row!

That’s the ability of the auto-placement algorithm of CSS Grid. If you outline the column for an merchandise, the following one shall be mechanically positioned after it! We don’t have to manually specify a bunch of columns and rows for all of the gadgets.

Bettering the Implementation

You don’t like these verbose :nth-child() selectors, proper? Me too, so let’s take away them and have a greater implementation. Such a pyramid is well-known within the math world, and we've one thing referred to as a triangular quantity that I'm going to make use of. Don’t fear, I cannot begin a math course, so right here is the system I shall be utilizing:

j*(j + 1)/2 + 1 = index

…the place j is a optimistic integer (zero included).

In concept, all of the :nth-child might be generated utilizing the next pseudo code:

for(j = 0; j< ?? ;j++) {
  :nth-child(j*(j + 1)/2 + 1) { grid-column-start: N - j }
}

We don’t have loops in CSS, so I'll observe the identical logic I did within the earlier article (which I hope you learn, in any other case you're going to get a bit misplaced). I categorical j utilizing the index. I solved the earlier system, which is a quadratic equation, however I'm positive you don’t wish to get into all that math.

j = sqrt(2*index - 1.75) - .5

We will get the index utilizing the sibling-index() operate. The logic is to check for every merchandise if sqrt(2*index - 1.75) - .5 is a optimistic integer.

.container {
  --s: 40px; /* measurement  */
  --g: 5px; /* hole */

  container-type: inline-size; /* we make it a container to make use of 100cqw */
}
.container > * {
  --_n: spherical(down,(100cqw + var(--g))/(2*(var(--s) + var(--g))));
  --_j: calc(sqrt(2*sibling-index() - 1.75) - .5);
  --_d: mod(var(--_j),1);
  grid-column-start: if(type(--_d: 0): calc(var(--_n) - var(--_j)););
}

When the --_d variable is the same as 0, it signifies that --_j is an integer; and when that’s the case I set the column to N - j. I don’t want to check if --_j is optimistic as a result of it’s all the time optimistic. The smallest index worth is 1, so the smallest worth of --_j is 0.

Tada! We changed all of the :nth-child() selectors with three traces of CSS that cowl any variety of gadgets. Now let’s make it responsive!

The Responsive Habits

Again in my 2021 article, I switched between the pyramidal grid and the basic grid primarily based on display measurement. I'll do one thing totally different this time. I'll maintain constructing the pyramid till it’s now not doable, and from there, it'll flip into the basic grid.

Showing a stack of hexagon shapes arranged in two shapes: on top is the pyramid grid and below that it becomes a rectangular grid.

Gadgets 1 to twenty-eight kind the pyramid. After that, we get the identical basic grid we constructed within the earlier article. We have to goal the primary gadgets of some rows (29, 42, and so on.) and shift them. We're not going to set a margin on the left this time, however we do have to set their grid-column-start worth to 2.

As ordinary, we determine the system of the gadgets, categorical it utilizing the index, after which take a look at if the result's a optimistic integer or not:

N*i + (N - 1)*(i - 1) + 1 + N*(N - 1)/2 = index

So:

i = (index - 2 + N*(3 - N)/2)/(2*N - 1)

When i is a optimistic integer (zero excluded), we set the column begin to 2.

.container {
  --s: 40px; /* measurement  */
  --g: 5px; /* hole */

  container-type: inline-size; /* we make it a container to make use of 100cqw */
}
.container > * {
  --_n: spherical(down,(100cqw + var(--g))/(2*(var(--s) + var(--g))));

  /* code for the pyramidal grid */
  --_j: calc(sqrt(2*sibling-index() - 1.75) - .5);
  --_d: mod(var(--_j),1);
  grid-column-start: if(type(--_d: 0): calc(var(--_n) - var(--_j)););

  /* code for the responsive grid */
  --_i: calc((sibling-index() - 2 + (var(--_n)*(3 - var(--_n)))/2)/(2*var(--_n) - 1));
  --_c: mod(var(--_i),1);
  grid-column-start: if(type((--_i > 0) and (--_c: 0)): 2;);
}

In contrast to the --_j variable, I would like to check if --_i is a optimistic worth, as it may be adverse for some index values. Because of this, I've an additional situation in comparison with the primary one.

However wait! That’s no good in any respect. We're declaring grid-column-start twice, so solely certainly one of them will get used. We must always have just one declaration, and for that, we will mix each situations utilizing a single if() assertion:

grid-column-start:
if(
  type((--_i > 0) and (--_c: 0)): 2; /* first situation */
  type(--_d: 0): calc(var(--_n) - var(--_j)); /* second situation */
);

If the primary situation is true (the responsive grid), we set the worth to 2; else if the second situation is true (the pyramidal grid), we set the worth to calc(var(--_n) - var(--_j)); else we do nothing.

Why that specific order?

As a result of the responsive grid ought to have a better precedence. Examine the determine under:

Showing how a stack of hexagon shapes arranged in a pyramid grid needs to respond to changes in screen size, highlighting on hexagon on the left edge and how it needs to adjust according to the new layout.

Merchandise 29 is a part of the pyramidal grid because it’s the primary merchandise in its row. Which means the pyramidal situation will all the time be true for that merchandise. However when the grid turns into responsive, that merchandise turns into a part of the responsive grid, and the opposite situation can be true. When each situations are true, the responsive situation one ought to win; that’s why it’s the primary situation we take a look at.

Let’s see this in play:

Oops! The pyramid seems good, however after that, issues get messy.

To grasp what is occurring, let’s look particularly at merchandise 37. If you happen to test the earlier determine, you'll discover it’s a part of the pyramidal construction. So, even when the grid turns into responsive, its situation continues to be true and it will get a column worth from the system calc(var(--_n) - var(--_j)) which isn't good as a result of we wish to maintain its default worth for auto-placement. That’s the case for a lot of gadgets, so we have to repair them.

To search out the repair, let’s see how the values within the pyramid behave. All of them observe the system N - j, the place j is a optimistic integer. If, for instance, N is the same as 10 we get:

10, 9, 8, 7, ... ,0, -1 , -2

At sure factors, the values grow to be adverse, and since adverse values are legitimate, these gadgets shall be randomly positioned, disrupting the grid. We have to make sure the adverse values are ignored, and the default worth is used as an alternative.

We use the next to maintain solely the optimistic worth and rework all of the adverse ones into zeroes:

max(0, var(--_n) - var(--_j))

We set 0 at the least boundary (extra on that right here) and the values grow to be:

10, 9, 8, 7, ... , 0, 0, 0, 0

We both get a optimistic worth for the column or we get 0.

However you mentioned the worth must be the default one and never 0.

Sure, however 0 is an invalid worth for grid-column-start, so utilizing 0 means the browser will ignore it and fall again to the default worth!

Our new code is:

grid-column-start:
  if(
    type((--_i > 0) and (--_c: 0)): 2; /* first situation */
    type(--_d: 0): max(0,var(--_n) - var(--_j)); /* second situation */
  );

And it really works!

You may add as many gadgets as you need, resize the display, and every thing will match completely!

Extra Examples

Sufficient code and math! Let’s get pleasure from extra variations utilizing totally different shapes. I’ll allow you to dissect the code as homework.

Rhombus grid

You'll discover a barely totally different strategy for setting the hole between the weather within the subsequent three demos.

Octagon grid

Circle grid

And the opposite hexagon grid:

Conclusion

Do you keep in mind after I advised you that we have been one property away from finishing the grid? That one property (grid-column-start) took us actually the entire article to debate! This demonstrates that CSS has developed and requires a brand new mindset to work with. CSS is now not a language the place you merely set static values such shade: pink, margin: 10px, show: flex, and so on.

Now we will outline dynamic behaviors by way of advanced calculations. It’s a complete strategy of considering, discovering formulation, defining variables, creating situations, and so forth. That’s not one thing new since I used to be in a position to do the identical in 2021. Nevertheless, we now have stronger options that permit us to have much less hacky code and extra versatile implementations.

Related Articles

Latest Articles