CSS & layout

One grid, five rounds, no track matching

One grid, five rounds, no track matching

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)

Related
CSS & layout

CSS anchor positioning: two properties do the wiring

Anchor positioning has landed in every major browser engine. Two properties, a keyword grid, and a fallback list replace the JavaScript most floating-UI widgets used to need.

July 7, 2026
CSS & layout

Grid Lanes gets a Field Guide — and a fourth layout primitive to argue about

WebKit has published gridlanes.webkit.org, a Field Guide for the new display: grid-lanes layout primitive, with a playground, a cheat sheet and six demos pitting Grid Lanes against Flexbox, Multicolumn and Grid.

June 22, 2026
CSS & layout

border-shape is the property clip-path always owed you

The upcoming CSS border-shape accepts the same values as clip-path, but border, box-shadow and outline follow the shape instead of getting clipped off. It is the missing half of the CSS shapes story — Chromium-only for now.

July 8, 2026