For years, building a responsive hexagon grid meant fighting floats, zero font sizes, and magic numbers that fell apart the moment a client widened their container. I have inherited production sites held together by strings of clip-path polygons and hardcoded media queries. That renders fine until the first content change nobody warned you about.
The row shifting a honeycomb needs used to take workarounds of its own. That has changed. Between corner-shape, sibling-index() and the trigonometric functions, the browser can now do the arithmetic that used to sit in a stylesheet as hardcoded numbers. If you have already been moving layout logic out of JS and into CSS, this is the next thing to try.
The modern hexagon shape
The old way was clip-path: polygon(). Precise, but with one nasty limitation: you cannot easily give it a border, because the clip-path removes the border along with everything else outside the shape. Getting a visible edge meant nesting an extra element or faking one with a pseudo-element.
corner-shape changes that. Paired with border-radius it produces a bevel that respects the element’s real box model, so an ordinary border declaration works.
.hexagon {
width: 100px;
/* Use trig functions to handle the geometry precisely */
aspect-ratio: cos(30deg);
border-radius: 50% / 25%;
corner-shape: bevel;
border: 2px solid var(--primary-color);
}
One caveat: corner-shape is Chrome and Edge only in early 2026. If you need wider browser support, clip-path still has its place. The aspect-ratio: cos(30deg) part works either way, and it beats leaving 1.1547 in a stylesheet for the next person to decode.
A logic-driven responsive hexagon grid
The hard part of a responsive hexagon grid is the overlap, not the shape. Every even row has to shift left by half an item’s width for the rows to interlock. When the grid is genuinely responsive you have no way of knowing which item starts an even row, because the number of items per row changes with the viewport.
The old answers were JavaScript or giving up and fixing the column count. sibling-index() together with container-type: inline-size lets the stylesheet work out where each item sits. Same idea as responsive circular avatar lists, with heavier arithmetic.
.container {
--s: 120px; /* Size */
--g: 10px; /* Gap */
display: flex;
flex-wrap: wrap;
gap: var(--g);
container-type: inline-size;
}
.container > * {
/* Calculate how many items fit in a row (N) and the shifted row (M) */
--_n: round(down, (100cqw + var(--g)) / (var(--s) + var(--g)));
--_m: round(down, (100cqw - (var(--s) - var(--g)) / 2) / (var(--s) + var(--g)));
/* Identify if the current item is the start of a shifted row */
--_i: calc((sibling-index() - 1 + var(--_m)) / (var(--_n) + var(--_m)));
--_c: round(down, 1 - mod(var(--_i), 1));
margin-left: calc(var(--_c) * (var(--s) + var(--g)) / 2);
margin-bottom: calc(var(--s) / (-4 * cos(30deg)));
}
Why this holds up
The reason to build it this way is stability. With the row-shift logic living in custom properties and math functions, there is no JavaScript grid calculator to race against and nothing shifting on screen while it runs. The same structure handles rhombuses, octagons and circles: adjust the aspect-ratio and the margin-bottom.
If hexagon grid geometry is eating your dev hours, I have been doing this since the 4.x days and I am happy to take it off your plate.
Refactoring your grid logic
Fourteen years of fixing “broken” WordPress sites taught me that most of them were not broken, just built on fragile CSS. aspect-ratio and corner-shape make a theme harder to knock over, and sibling-index() retires the nth-child hacks that fall apart the moment AJAX adds an element.