CSS grid

Understand CSS Grid through tracks, lines, and auto-placement, then handle responsive columns, overflow, visual order, and generated-code failures.

level intermediate time 12 min at Standard depth
version CSS Grid Layout Module Level 2
what

CSS Grid is a two-dimensional layout model. A grid container places its direct children into a grid made of row and column tracks.

trap

1fr does not automatically override content minimums, and dense packing or explicit lines can make visual order diverge from DOM and keyboard order.

fix

Define source order and track constraints first, then inspect implicit tracks, longest content, and the narrowest container; use minmax(0, 1fr) or min-inline-size: 0 when shrinking is intentional.

What it is and why it exists

CSS Grid Layout is a two-dimensional layout model for alignment across rows and columns. An element with display: grid becomes a grid container , and its direct children become grid items . Deeper descendants do not automatically participate in that grid.

Grid solves structures shared across two axes. A page shell can make its header span a sidebar and main content, a card collection can share column widths, and form labels can align across rows. With a one-dimensional layout, each row often calculates sizes independently and cannot guarantee that the next row follows the same columns.

Flexbox is usually more direct when layout distributes space mainly in one direction; Grid is usually clearer when both rows and columns are constraints. They can be nested: Grid can own the two-dimensional structure between components while Flexbox owns one row of buttons inside a component.

Grid changes presentation, not document meaning. Source order should still express content and task order, and HTML elements still need correct heading, navigation, main-content, and control semantics. Do not use grid placement to patch a faulty DOM structure.

How it works

A grid consists of row and column tracks between adjacent grid lines. A grid track is the space between two neighboring grid lines; row and column lines enclose grid cells, and contiguous rectangular cells form a grid area. An item can enter the next available cell automatically or be placed explicitly by line or named area.

The flow below is a debugging model, not a literal transcription of the browser’s specification algorithm. Establish the explicit grid and definitely positioned items first, then let auto-placement fill positions; overflow beyond the boundary creates implicit tracks, after which the browser has enough information to resolve track sizes and alignment.

Explicit and implicit grids

The part defined by grid-template-columns, grid-template-rows, or grid-template-areas is the explicit grid . grid-template-columns: 12rem 1fr explicitly creates two columns, while grid-template-rows: auto 1fr auto creates three rows. Positive line numbers start from the explicit grid’s start edge; negative numbers count backward from its end edge.

When items outnumber explicit cells or are assigned beyond the explicit boundary, the browser creates implicit grid tracks. grid-auto-rows and grid-auto-columns size those tracks; without them, implicit tracks are auto-sized. Unexpected horizontal scrolling often comes not from the template columns but from an item that created an implicit column.

Track sizing and fr

Tracks can use fixed lengths, percentages, content keywords, or flexible units. fr represents a share of flexible leftover space in the grid container, not a fraction of the entire container width. Fixed tracks and gap consume space first, then remaining space is divided by fr ratios, so the two flexible tracks in 200px 1fr 2fr receive a 1:2 ratio.

Content can still impose a floor on a track. A plain 1fr track has an automatic minimum, so a long unbreakable string or intrinsically sized content may force it wider. Use minmax(0, 1fr) only when the design genuinely allows that content region to shrink, then choose a wrapping, clipping, or scrolling policy for the item.

minmax(minimum, maximum) gives a track a range. repeat(auto-fit, minmax(min(100%, 16rem), 1fr)) creates as many columns as the container can hold while allowing a column to fall to 100% when the container is narrower than 16rem. The inner min() prevents the minimum track size itself from exceeding the container.

Both auto-fill and auto-fit calculate how many repeated tracks fit. With fewer items than tracks, auto-fill preserves empty tracks; auto-fit collapses them so existing items can use the released space. They look the same while items occupy every track, so test with fewer items before choosing one.

Placement and named areas

grid-column and grid-row position items by grid line, with the ending line excluded from the span. grid-column: 1 / 3 spans the two column tracks between lines 1 and 3, while grid-column: span 2 spans two tracks from an automatic or specified start. -1 means the final line of the explicit grid, not the end of an indefinitely growing implicit grid.

grid-template-areas draws rectangular areas with repeated names, and items enter them through grid-area. Every template row must have the same number of cells, and each repeated name must form one rectangle; otherwise the entire declaration is invalid. Named areas fit stable page shells, while grid lines fit repeated items and programmatic spans.

Items without a definite position use the auto-placement algorithm. The default grid-auto-flow: row searches along the inline direction before creating a new row; column swaps the primary filling direction. Adding dense lets later, smaller items backfill earlier holes, changing visual order without changing DOM order.

Alignment and gaps

justify-items and justify-self align items within their grid areas on the inline axis; align-items and align-self do so on the block axis. The inline axis is not always horizontal, nor is the block axis always vertical; writing-mode and text direction affect both. place-items is shorthand for align-items and justify-items.

justify-content and align-content align the track collection, not individual items inside tracks. They produce a visible difference only when the container has distributable space on the relevant axis. gap creates gutters only between tracks; padding still owns the inset around the container’s edges.

Named lines and maintainable constraints

Numeric lines fit small, stable grids, but numbers in a page shell hide what each boundary means. Square brackets give one grid line one or more names, and items can then use those names for placement. Names belong to lines rather than tracks, so content-start and content-end express the two edges of the content track.

CSS
.workspace {
  display: grid;
  grid-template-columns:
    [sidebar-start] minmax(12rem, 18rem)
    [sidebar-end content-start] minmax(0, 1fr)
    [content-end];
  gap: clamp(1rem, 3vw, 2rem);
}

.workspace > main {
  grid-column: content-start / content-end;
  min-inline-size: 0;
}

This template constrains the sidebar between 12rem and 18rem and lets the content track shrink to a zero floor. clamp() adjusts only the gutter, not the number of tracks. Named boundaries communicate intent better than grid-column: 2 / 3 and reduce cascading edits when a new track is inserted.

The same name can occur on several lines, and repeat() can generate repeated names; a name plus an integer then selects a particular occurrence. Prefer unique names in a simple shell. For repeated grids, favor auto-placement and span instead of generating a brittle absolute line number for every item.

Spans, overlap, and paint order

Several items can occupy the same grid area without position: absolute. They still participate in grid sizing and overlap in normal painting order; grid items can also use z-index to state their stacking order. Overlap fits a title or status badge on an image, but it must not leave an obscured interactive control in the focus order.

CSS
.hero {
  display: grid;
  grid-template: "stack" minmax(14rem, 40vh) / minmax(0, 1fr);
}

.hero > img,
.hero > .caption {
  grid-area: stack;
}

.hero > .caption {
  z-index: 1;
  align-self: end;
  padding: 1rem;
}

The image and caption share the stack area, and the caption uses z-index to sit above the image. Source order should still put the image before the caption that describes it; if an overlay opens and closes, its hidden state must coordinate visibility, pointer hit testing, and keyboard focus.

Examples

Build a page shell with named areas

This shell has one fixed sidebar and one flexible main column. Its header spans both columns, and the script reads browser-computed tracks and item widths to confirm that the gap has already been removed from flexible space.

named-shell.html
<section class="page-shell">
  <header>Header</header>
  <nav>Navigation</nav>
  <main>Main</main>
</section>

<style>
  .page-shell {
    display: grid;
    grid-template-columns: 180px 1fr;
    grid-template-rows: 40px 120px;
    grid-template-areas:
      "header header"
      "nav main";
    column-gap: 16px;
    inline-size: 620px;
  }
  header { grid-area: header; }
  nav { grid-area: nav; }
  main { grid-area: main; }
</style>

<script>
  const shell = document.querySelector('.page-shell');
  const nav = document.querySelector('nav');
  const main = document.querySelector('main');
  const columns = getComputedStyle(shell).gridTemplateColumns.split(' ');

  console.log(`columns=${columns.join(',')}`);
  console.log(`header=${document.querySelector('header').clientWidth}; nav=${nav.clientWidth}; main=${main.clientWidth}`);
</script>
columns=180px,424px
header=620; nav=180; main=424

After the 620px container gives 180px to the sidebar and 16px to the gap, the main column receives 424px. Named areas neither copy content nor change source order; they only map the three direct children into grid areas.

Respond to the card container’s width

The card grid uses auto-fit: the wide container holds four columns, while the narrow container holds two. The example changes the container’s own logical width, so it verifies component space rather than one fixed viewport breakpoint.

responsive-cards.html
<section class="cards" aria-label="Plans">
  <article>Free</article>
  <article>Team</article>
  <article>Business</article>
  <article>Enterprise</article>
</section>

<style>
  .cards {
    display: grid;
    grid-template-columns:
      repeat(auto-fit, minmax(min(100%, 160px), 1fr));
    gap: 12px;
    inline-size: 700px;
  }
  .cards article {
    box-sizing: border-box;
    min-inline-size: 0;
    padding: 8px;
  }
</style>

<script>
  const cards = document.querySelector('.cards');
  const tracks = () => getComputedStyle(cards).gridTemplateColumns;

  console.log(`wide=${tracks()}`);
  cards.style.inlineSize = '340px';
  console.log(`narrow=${tracks()}`);
</script>
wide=166px 166px 166px 166px
narrow=164px 164px

Three 12px gaps in the 700px container leave 664px, making each column 166px. At 340px, one gap leaves 328px and each column becomes 164px; the remaining items enter the next row.

Observe dense visual reordering

The two wide items span two columns each. dense lets the later narrow item C backfill the hole in the first row, so coordinate-sorted visual order no longer matches DOM order.

dense-placement.html
<section class="queue" aria-label="Processing order">
  <div class="wide">A</div>
  <div class="wide">B</div>
  <button type="button">C</button>
  <button type="button">D</button>
</section>

<style>
  .queue {
    display: grid;
    grid-template-columns: repeat(3, 80px);
    grid-auto-rows: 32px;
    grid-auto-flow: row dense;
    gap: 8px;
  }
  .wide { grid-column: span 2; }
</style>

<script>
  const items = [...document.querySelector('.queue').children];
  const visual = [...items].sort((left, right) =>
    left.offsetTop - right.offsetTop || left.offsetLeft - right.offsetLeft
  );

  console.log(`dom=${items.map((item) => item.textContent).join('>')}`);
  console.log(`visual=${visual.map((item) => item.textContent).join('>')}`);
</script>
dom=A>B>C>D
visual=A>C>B>D

Keyboard focus still visits buttons C and D in DOM order, and assistive technology does not rewrite reading order from visual coordinates. dense fits decorative collections whose order does not matter, not forms, procedures, or action rows that need reordering.

Let long content actually shrink

The second track uses minmax(0, 1fr), and the item that owns the text also has a zero logical minimum. The unbreakable repository name therefore stays inside a 200px track, where the component deliberately chooses ellipsis.

shrinkable-result.html
<article class="result">
  <div class="icon" aria-hidden="true">CSS</div>
  <div class="details">
    <strong>Repository</strong>
    <div class="repository">frontend-platform-with-a-very-long-name</div>
  </div>
</article>

<style>
  .result {
    display: grid;
    grid-template-columns: 48px minmax(0, 1fr);
    gap: 12px;
    inline-size: 260px;
  }
  .details { min-inline-size: 0; }
  .repository {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
</style>

<script>
  const row = document.querySelector('.result');
  const icon = document.querySelector('.icon');
  const details = document.querySelector('.details');
  const repository = document.querySelector('.repository');

  console.log(`row=${row.clientWidth}; icon=${icon.clientWidth}; details=${details.clientWidth}`);
  console.log(`truncated=${repository.scrollWidth > repository.clientWidth}`);
</script>
row=260; icon=48; details=200
truncated=true

The 260px width is exactly the 48px icon, 12px gap, and 200px details track. truncated=true proves that the content is wider than its visible area; text-overflow: ellipsis alone could not produce that result without shrinkable space.

Pitfalls

Fix: find the item that actually owns the content. When the design allows shrinking, use minmax(0, 1fr) or min-inline-size: 0, then choose wrapping, clipping, or scrolling explicitly instead of hiding every overflow.

Fix: use repeat(3, 1fr) to divide leftover space. If percentage tracks are required, include gutters in the sizing calculation and measure the container’s scroll width at the narrowest supported width.

Fix: make source order satisfy content and task logic first. Use dense only for order-independent items, then check the result with a keyboard, a screen reader, and an unstyled page.

Fix: inspect the template and every explicit placement together for each layout variant. View actual tracks in developer tools and assert that an accidental implicit column does not make scrollWidth exceed clientWidth.

Fix: tokenize and compare every row, then confirm that each name makes one rectangle. Read getComputedStyle(container).gridTemplateAreas instead of trusting a source template that merely looks grid-shaped.

Deep Subgrid shares parent tracks

Subgrid shares parent tracks

An ordinary nested grid calculates its tracks independently, so headings and action rows in adjacent cards may not align. A subgrid makes a nested grid adopt the parent’s track sizes on a selected axis. Children still belong to the nested grid, but their lines correspond to the parent tracks spanned by that nested grid.

Each card below spans three parent row tracks, then uses those rows as its own subgrid. Titles, content, and footers from different cards therefore contribute to one set of parent row sizes, while each card still handles its column direction independently.

CSS
.catalog {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-template-rows: auto 1fr auto;
  gap: 1rem;
}

.card {
  display: grid;
  grid-row: span 3;
  grid-template-rows: subgrid;
}

subgrid replaces an independent track list on the selected axis, so the same declaration cannot append another set of local sizes. A subgrid uses the parent grid’s gutters by default but can set its own gap. Use it for an axis that genuinely needs cross-component alignment; an ordinary nested grid is clearer when each card should keep its own rhythm.

Subgrid changes neither semantics nor ownership. Each card still needs a complete, sensible DOM order, and the parent tracks must provide the positions it is meant to span. Inspect parent and subgrid overlays together when debugging instead of looking only at the inner computed style.

Further reading

checkpoint

4 questions · 1 predict-the-output · 1 spot-the-bug

Copy as Markdown Interview bank Edit on GitHub Report an error Was this clear?