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.
Table of Contents
- 1. grid-auto-flow basics: how automatic item placement works
- 2. The sparse algorithm in detail: why gaps appear without dense
- 3. The dense packing algorithm: how it searches backward and fills gaps
- 4. A practical recipe: a photo gallery with mixed tile sizes
- 5. Visual order vs. DOM order: when dense changes the arrangement
- 6. Accessibility implication: why tab order and visual order diverge
- 7. When dense makes sense and when to avoid it
- 8. Combining dense with grid-auto-rows, grid-auto-columns and an explicit grid
- 9. Debugging dense: tools and approach for unexpected jumps
- 10. Summary
- 11. FAQ
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.