The Flexible Thumbnail Grid Problem

CSS layout has long been a source of frustration for developers, even those comfortable with more mathematical CSS features like transforms. A particularly tricky problem: creating a grid of fixed-size thumbnails that remains centered relative to surrounding text while the last row's items align left — all with a grid width that depends on the viewport.

Why Traditional Techniques Fail

Floats were an early dead end. While they handle left alignment of the last row, they offer no mechanism for centering the grid as a whole.

inline-block seemed more promising. But setting text-align: center on the container centers every row — there was no obvious way to keep the final row left-aligned.

A CSS property called text-align-last appeared to offer a solution. Setting text-align-last: left on the grid didn't work. Combining text-align: justified with text-align-last: left only produced acceptable results when the last row had exactly one item and the column gaps weren't too wide.

Flexbox offered hacky workarounds. One approach added an ::after pseudo-element to the grid with flex: 1 — the same flex: 1 applied to each thumbnail. This creates a layout that looks acceptable only when the last row is missing exactly one item. If the final row is missing more items or is completely full, the layout breaks.

A variation of this idea involved placing empty

elements after the thumbnails, approximating the expected number of columns. Since a thumbnail width is fixed and you can cap the post's maximum width for readability, you could estimate the maximum column count. The first empty elements occupy full row width while the rest spill into invisible zero-height rows. This works but produces ugly gaps and remains fundamentally fragile.

The Media Query Solution

CSS Grid seemed like the logical answer, but common examples all relied on predefined column counts — not suitable for a grid where the number of columns depends on the viewport width. One creative workaround: generate a series of media queries that modify a CSS variable (--n) representing the column count used in grid-template-columns.

/* Simplified illustration of the technique */
@media (min-width: 10em) { --n: 1 }
@media (min-width: 20em) { --n: 2 }
@media (min-width: 30em) { --n: 3 }

While functional, this approach is far from elegant: one media query per possible column count, and it breaks down when the grid width doesn't equal the viewport width but rather depends on sibling element widths.

The auto-fit Revelation

A better solution emerged during a frustrating session with CSS Grid. While investigating why the repeat() function wasn't behaving as expected, the documentation led to the auto-fit keyword. Despite unclear documentation, the potential for this problem was immediately apparent.

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8em, 1fr));
  justify-content: center;
  gap: 1em;
}

The minmax() function also plays a critical role here. In this context, it creates the grid and then stretches the columns equally until they fill the available space.

Making the thumbnail width responsive requires an min() function to prevent overflow:

grid-template-columns: repeat(auto-fit, minmax(min(8em, 100%), 1fr));

This ensures images never exceed 100% of the container but never shrink below 8em. Note the min() function does not work in pre-Chromium Edge browsers.

Handling Panels and Sizing Variations

One limitation of this technique: it only looks clean when all images share the same aspect ratio. For images with different aspect ratios, the grid becomes inconsistent:

  • Fixing image heights distorts the images
  • Setting object-fit: cover solves the distortion problem while maintaining the visual structure

The flexibility extends gracefully if you want certain elements to span multiple columns. Since the column count is viewport-dependent, you can't hardcode a grid-column-end value — but you can use negative indices:

.banner {
  grid-column-end: -1;  /* spans to the last column */
}

Making the first item a full-width banner with a larger height is simple. Setting grid-column-end: -2 spans everything except the last column, and other negative values work similarly.

Auto-fill vs. Auto-fit

The keywords auto-fill and auto-fit seem interchangeable — their documentation is dense, and swapping one for another can produce zero visual difference. The distinction emerges in specific scenarios:

  • With a fixed, non-minmax() column width like 8em, the two often produce identical results when there are enough items to require more than one row
  • The difference becomes apparent when you have only a few items. If the grid uses a minmax() column width rather than a fixed one, the results differ

The choice between them may ultimately come down to preference. With justify-content: center, auto-fill seems to be the more logical option, but auto-fit produces a better-looking result in certain configurations. The practical takeaway: if the grid uses a flexible minmax() column definition and contains only a few items, prefer auto-fill. The defaults strike the best balance with relatively simple rules, providing clean behavior across all these layout challenges without resorting to media query hacks or fragile pseudo-elements.