grid-auto-flow: dense Explained: Understanding the Packing Algorithm
AI generated
{ }
@
CSS · Grid Layout · Accessibility
grid-auto-flow: dense
Understanding the packing algorithm instead of just using it

grid-auto-flow: dense automatically fills gaps in a CSS grid by searching backward on every placement. That produces a tight, gapless layout, but it can separate the visual order of elements clearly from their order in the markup, with direct consequences for keyboard navigation and screen readers.

14 min read CSS Grid Level 1 Chrome · Firefox · Safari · Edge

1. grid-auto-flow basics: how automatic item placement works

grid-auto-flow controls how the browser places grid items that were not given an explicit grid-column or grid-row position. By default the property is row: the algorithm fills a row from left to right and only moves to the next row once the current one is full. With the value column the direction flips, items get distributed column by column before the browser moves to the next column.

The dense keyword switches on a second mode that actively searches the already laid-out grid for gaps and fills them instead of leaving them open. In practice dense shows up almost always in grids with differently sized tiles, for example photo galleries where individual items span two columns or two rows. Without dense, such layouts almost inevitably end up with empty cells, because the default algorithm never places an item backward, even when there is free space earlier in the grid.

2. The sparse algorithm in detail: why gaps appear without dense

The default algorithm, called sparse in the specification, remembers only a single cursor position in the grid for every automatic placement and moves it strictly forward. For each new item the browser searches from that cursor position for the first free cell large enough for the item, places it there, and advances the cursor accordingly. A free cell that was already passed is never touched again, even if a later, smaller item would fit into it exactly.

This behavior is deliberate, because it leaves DOM order untouched: item number five in the markup is guaranteed to land after item number four in the grid, never before it. The price is visual inefficiency. In a grid mixing single- and double-column tiles, sparse regularly leaves individual cells empty, because the cursor already passed a smaller gap before a matching item followed. In form or dashboard layouts with equally sized cells the effect barely shows, in tile layouts it clearly does.

3. The dense packing algorithm: how it searches backward and fills gaps

With grid-auto-flow: row dense the browser switches into a backward search mode for every item to be placed. Instead of continuing only from the last cursor position, the algorithm checks the entire grid laid out so far from the beginning, finds the first free cell large enough for the current item, and places it there, even if that means it lands ahead of an already placed item that appears later in the markup.

This mechanism explains why dense grids almost always show fewer gaps than sparse grids with identical markup: every newly created gap is immediately considered a candidate on the next placement. The downside is the same mechanism working in reverse, an item can appear visually well ahead of its markup position if a matching gap happened to be open earlier in the grid. For purely decorative layouts that is harmless, for content whose order carries meaning it is a real problem.


/* Without dense: item 4 waits for the next free row,
   even though there is room in row 1 after the wide item. */
.gallery {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: 120px;
  grid-auto-flow: row;
  gap: 1rem;
}

/* With dense: the algorithm searches backward and
   fills exactly that gap with the next matching item. */
.gallery--dense {
  grid-auto-flow: row dense;
}

.gallery__item--wide { grid-column: span 2; }
.gallery__item--tall  { grid-row: span 2; }

4. A practical recipe: a photo gallery with mixed tile sizes

For a typical photo gallery, start by defining a base grid with a fixed number of columns and a fixed grid-auto-rows height, so every cell shares the same base unit. Individual tiles get a larger footprint through modifier classes like wide (span 2 columns) or tall (span 2 rows). Without dense, small holes almost inevitably appear at the edges of these larger tiles, looking like a layout bug even though the algorithm is technically working correctly.

Once grid-auto-flow is set to row dense, subsequent smaller tiles automatically move into those holes, and the grid appears closed without maintaining a manual position for every combination of large and small tiles. It matters to set grid-auto-rows explicitly, otherwise the browser computes each implicit row height from the tallest content that lands in it, which can produce unevenly sized rows in dense layouts because different passes pack different items into the same row.


.gallery {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: 140px;
  grid-auto-flow: row dense;
  gap: 1rem;
}

.gallery__item--wide { grid-column: span 2; }
.gallery__item--tall  { grid-row: span 2; }

@media (max-width: 640px) {
  .gallery {
    grid-template-columns: repeat(2, 1fr);
  }
}

5. Visual order vs. DOM order: when dense changes the arrangement

The crucial side effect of dense is that an item's visual position no longer necessarily matches its position in the markup. An item listed tenth in the HTML can visually appear third in the grid through a matching gap, if earlier, larger items left room at that spot. For sighted mouse users that often reads as pleasantly compact, since the layout shows no holes and looks visually tidy.

It becomes problematic once the order of content carries meaning, for example a numbered article overview, a ranking, or a chronological list. The specification explicitly warns against using dense for content-ordered lists, because the algorithm is optimized purely for geometric gap filling and takes no account of semantic order whatsoever. Anyone who wants the visual density of dense but cannot afford to lose the order needs to arrange the data before rendering so the resulting reshuffle stays acceptable.

6. Accessibility implication: why tab order and visual order diverge

Keyboard navigation and screen readers fundamentally follow DOM order, not the visual presentation CSS produces. grid-auto-flow: dense only changes where an item lands on screen, not its position in the document tree. The result: pressing tab still strictly walks through the tiles in markup order, but visually it can jump back and forth across the grid, because the visual arrangement was changed by dense while the logical order stayed the same.

This exact divergence is what WCAG success criterion 1.3.2 (Meaningful Sequence) addresses, requiring a programmatically determinable reading order to be preserved whenever it matters for understanding the content. For sighted keyboard users the jumping focus outline creates a confusing visual pattern, for screen reader users the read-out order stays correctly tied to DOM order but may clash with what sighted companions currently see on screen. In both cases dense should only be used for purely decorative, meaning-free arrangements.


<style>
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-auto-flow: row dense;
  gap: 1rem;
}
.grid a:focus-visible {
  outline: 3px solid #7c3aed;
  outline-offset: 2px;
}
</style>

<!-- Tab order stays 1, 2, 3, the focus outline still
     jumps visually across the grid because link 1 takes
     up more space visually thanks to span 2. -->
<nav class="grid">
  <a href="/topic-1" style="grid-column: span 2;">First topic</a>
  <a href="/topic-2">Second topic</a>
  <a href="/topic-3">Third topic</a>
</nav>

7. When dense makes sense and when to avoid it

dense fits well for layouts where the order of elements carries no content meaning, for example a moodboard, a plain image gallery without a timeline, or a grid of interchangeable promo tiles. In such cases the benefit of a gapless, visually tight layout clearly outweighs the downside of a changed focus order, since nobody expects tile three to build on tile two anyway.

Avoid dense, on the other hand, for pricing tables, blog overviews with publication dates, forms with logically dependent fields, or any context where users need a predictable reading or tab order. As a compromise, dense can often be applied selectively to just part of a grid, for example a pure image strip inside an otherwise sequential article, rather than switching the whole page to dense.

8. Combining dense with grid-auto-rows, grid-auto-columns and an explicit grid

dense only reaches its full effect together with grid-auto-rows and grid-auto-columns, which set the size of the implicitly created tracks. Without an explicit height, the browser computes each automatic row from the tallest content that lands in it, which can produce inconsistent row heights in dense layouts, because different passes pack different items into the same row.

In mixed layouts with an explicit grid area for core elements, for example a headline and lead paragraph with a fixed position, and an implicit, densely packed area for supplementary content, it pays to separate the two areas clearly through named grid lines. That keeps the explicit part independent of the packing algorithm, while only the dynamically filled area benefits from dense, and the core area stays predictable in its position.


.layout {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  grid-template-rows: auto;
  grid-auto-rows: 100px;
  grid-auto-flow: row dense;
  gap: 0.75rem;
}

/* Fixed area, untouched by dense */
.layout__hero {
  grid-column: 1 / -1;
  grid-row: 1;
}

/* Dynamic area, this is where dense applies */
.layout__card--big {
  grid-column: span 2;
  grid-row: span 2;
}

9. Debugging dense: tools and approach for unexpected jumps

When a grid layout with dense produces unexpected results, it helps to enable the Firefox grid inspector overlay, which numbers and highlights every grid cell in color. That immediately shows which item landed in which cell, regardless of its markup position. Chrome and Edge offer a comparable overlay in DevTools that also shows row and column numbers in addition to the plain area and highlights cells on hover.

As a second check it pays to temporarily switch to grid-auto-flow: row without dense to compare how much the arrangement changes. If the difference is small, dense is usually harmless. If a particular item jumps several positions, check specifically whether that item carries a content order that must remain recognizable to users, and when in doubt set an explicit position instead of relying on automatic placement.

Aspect row (sparse) row dense Recommendation
Cursor behavior Forward only Backward search per item Use dense only for meaning-free order
Gaps in the grid Common with mixed sizes Actively filled Use dense for dense galleries
DOM vs. visual order Stays synchronized Can diverge significantly Test tab order beforehand
Typical use Forms, dashboards Image galleries, moodboards Choose per content type
WCAG relevance Not a concern Check 1.3.2 Meaningful Sequence Use only for decorative grids

Mironsoft

Modern CSS, layout architecture and rendering performance

CSS that stays maintainable instead of breaking with every change?

We review existing stylesheets for specificity chaos and layout thrashing, then build a CSS architecture with cascade layers, custom properties and modern layout primitives that still makes sense after the tenth feature.

CSS Audit

Systematically uncovering specificity issues, cascade conflicts and unused selectors.

Architecture Refactoring

Introducing cascade layers, custom properties and design tokens cleanly.

Performance Tuning

Fixing layout thrashing, expensive selectors and rendering bottlenecks.

10. Summary

grid-auto-flow: dense: The Essentials at a Glance

Sparse vs. dense

Sparse moves the placement cursor only forward, dense searches the entire grid backward for the first matching gap on every item.

Visual reordering

dense can place items well ahead of their DOM position, while tab order and screen readers still follow markup order.

WCAG 1.3.2

Meaningful Sequence requires a preserved reading order whenever it matters for understanding, dense should only be used for meaning-free layouts.

Practical recipe

Set grid-auto-rows explicitly, apply dense selectively to isolated image areas instead of the whole page, and test focus order before launch.

11. FAQ: grid-auto-flow: dense: The Essentials at a Glance

1What does grid-auto-flow: dense actually do differently from row?
dense searches the entire grid laid out so far from the start for the first matching gap on every placement, while row only moves the placement cursor forward and leaves passed gaps empty.
2Does dense change the order in the DOM?
No. dense only changes the visual position of items in the grid, the order in the document tree, and therefore tab and screen reader order, stays unchanged.
3Why should I avoid dense for blog lists?
Because the chronological or content order of the articles should stay recognizable to users. dense can place a later published article visually ahead of an earlier one if a matching gap happened to be open.
4How does dense relate to WCAG 1.3.2?
The success criterion requires a preserved, meaningful reading order. If visual order diverges significantly from DOM order, it can confuse keyboard users and screen reader users.
5Do I need grid-auto-rows when using dense?
Not strictly, but without an explicit row height the browser computes each implicit row from the tallest content, which can lead to unevenly sized rows with dense.
6Can I enable dense for only part of the grid?
Yes. dense is a property of the grid container, so you can create a separate nested grid container just for the dense-suited image area and leave the rest of the page sequential.
7Do all modern browsers support grid-auto-flow: dense?
Yes, dense is part of the CSS Grid Level 1 specification and is supported by all current versions of Chrome, Firefox, Safari and Edge.
8How do I find out which item ended up where in the grid?
The Firefox grid inspector and comparable overlays in Chrome and Edge number every grid cell and show which element occupies it, regardless of markup order.
9Does dense automatically fill every gap in the grid?
No. dense only fills gaps that a following item matches in size and shape. If no matching item remains, the cell stays empty regardless.
10Is there an alternative to dense for dense layouts without the order risk?
Yes, items can be sorted server-side or with JavaScript before rendering into an order that is already gapless, then row without dense keeps DOM and visual order synchronized.