One grid, five rounds, no track matching
Iris Calderón
You have a tournament bracket. Round of 32 on the outside, Round of 16 one step in, quarters, semis, and a final in the middle. Every round has to line up vertically with the ones next to it, or the whole thing reads as a shrug. Historically you built five separate grids and prayed the track widths matched. Or you did the whole thing in one enormous grid and did math on every match's grid-column.
A new source rebuilds that layout (the actual FIFA World Cup standings page) using CSS grid and subgrid. It opens DevTools on the existing page first, finds it built in a hacky way, then walks through a cleaner version. The interesting bit isn't the bracket. It's how subgrid lets each round own its own layout code without owning its own tracks.
One grid to share
The outer container defines the shape. Eight columns, four rows, tight gap.
.rounds {
display: grid;
grid-template-columns: repeat(8, minmax(30px, 1fr));
grid-template-rows: repeat(4, 32px);
gap: 0.5rem;
}
Eight columns because the bracket has four rounds per side plus the final, counting outward from the middle. Four rows because that's how many Round-of-32 matches stack on each side. minmax(30px, 1fr) keeps a column from collapsing when the viewport gets tight; the fixed 32px rows hold the vertical rhythm.
Rounds that inherit the tracks
Here's the piece subgrid actually solves. Each round is a real element in the DOM. You want it to be, for accessibility and for reasoning about the markup. But you don't want that element to invent its own column widths. You want it to plug into the parent's tracks.
.round-of-32 {
grid-column: 1 / -1;
grid-row: 1 / -1;
display: grid;
grid-template-columns: subgrid;
grid-template-rows: subgrid;
}
grid-template-columns: subgrid is the whole point. This element declares a grid, but it borrows columns from .rounds instead of defining new ones. Same for rows. So when a child of .round-of-32 says grid-column: 1, it lands on the outer grid's first column, not on some inner grid the browser had to invent.
That's how the round can be a semantic wrapper and still have its children position against the outer grid as if the wrapper wasn't there.
Negative line numbers do the trimming
The Round of 32 spans the full width but has content only at the two edges — one column of matches on the left, one on the right. The source places those with negative line numbers.
.start {
grid-column: 1;
grid-row: 1 / -1;
}
.end {
grid-column: -2;
grid-row: 1 / -1;
}
-1 is the final grid line. -2 is one line back. So .end sits in the last real column without you ever writing 8, which is the point. Round of 16 uses the same pattern to move inward by one column on each side.
.round-of-16 {
display: grid;
grid-template-columns: subgrid;
grid-template-rows: subgrid;
grid-column: 2 / -2;
grid-row: 1 / -1;
}
That's the pattern for the whole bracket. Every round is a subgrid child of .rounds, positioned by counting inward from either edge. Add a quarterfinals layer, grid-column: 3 / -3. Semis, 4 / -4. You never touch column counts. You count from the outside.
Matches that center themselves
Inside each side column, the matches stack. Half as many matches per round as the one outside it, which means they need to sit at the midpoints of the outer round's slots. The source hands the vertical centering to flexbox.
.start,
.end {
display: flex;
flex-direction: column;
}
.match {
flex: 1;
justify-content: center;
}
flex: 1 gives each match an equal share of the column's height, and justify-content: center pushes its content to the vertical middle. Grid holds the outer rhythm; flex handles the intra-column spacing. Different tools for different jobs, no fighting.
When the viewport gives up
At tablet sizes the source keeps the same layout and lets it scroll horizontally. At the smallest sizes it drops the bracket shape entirely. The outer grid switches to four equal columns and every round left-aligns.
.rounds {
grid-template-columns: repeat(4, 1fr);
}
All the Round-of-32 matches end up in the same column instead of splitting across left and right. The bracket becomes a list of rounds, which is what fits on a phone.
Worth noticing: the subgrid contract doesn't break when the outer grid changes shape. .round-of-32 still inherits whatever .rounds declares. Change the parent, the children follow. That's what makes the pattern worth reaching for. The responsive variant is a two-line change on one selector.
Try it on something less dramatic
You probably don't have a bracket. But subgrid pays off anywhere a nested element needs to line up with a sibling's tracks. A card that stretches its title, body and footer across a grid row so every card in the row shares baselines. A form label column that stays aligned across grouped fieldsets. A comment thread where every reply's timestamp lands on the same right edge.
The rule is the one the source uses. Give the outer element real tracks. Give the inner element grid-template-columns: subgrid (or rows). Then position children with line numbers, negative when you can, so the layout survives a new column dropping in later.
Source: Ahmad Shadeed (ishadeed.com)