Skip to content

Responsive CSS Grid with auto-fit, minmax, and repeat

Build a responsive CSS grid with zero media queries using repeat, auto-fit, and minmax. Learn intrinsic sizing, clamp, gap, and where it breaks.

· · 13 min read

Updated: October 16, 2026

Illustration of smartphone application screens

Quick Take

You build a responsive CSS grid without media queries by letting the grid size its own tracks: repeat(auto-fit, minmax(280px, 1fr)) reflows from many columns to one as the container shrinks, no breakpoints required.

Run this yourself. The measurement behind the breakpoint and scrollbar tables: css-auto-fit-breakpoints/ in the Coding Dunia code-examples repo. It asks Chromium for the resolved tracks at every container width and asserts the result.

You get a responsive CSS grid without media queries by describing the tracks you want instead of the exact number of columns. Write grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)), add a gap, and the browser packs in as many columns as fit at 280px each, then reflows to fewer columns, and finally to one, as the container narrows. The breakpoints happen automatically because the sizing is intrinsic. Card grids built this way go from four columns on a wide dashboard to a single column on a 360px phone without a single @media block. That's the whole trick. Everything below is why it works and where it bites.

How does repeat(auto-fit, minmax(280px, 1fr)) work?

This one-line value packs three separate mechanisms into a single declaration, and it reads like a spell until you see what each token does on its own. Intrinsic sizing is CSS letting an element compute its own size from content and available space rather than the developer hard-coding it, and that's exactly what auto-fit does for the column count here. Take it apart piece by piece below.

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1.5rem;
}

repeat() is a shorthand so you don't type the same track size over and over. Normally you'd pass a fixed count, like repeat(3, 1fr) for three equal columns. Here the count is a keyword instead. auto-fit tells the browser to compute the count itself based on how many tracks fit the container. Each generated track is sized by minmax(280px, 1fr), which means "never smaller than 280px, but grow to an equal share of leftover space." So the browser divides the available inline width by 280px plus the gap, floors that to a whole number, and lays out exactly that many columns. Widen the container and one more column pops in. Shrink it and a column drops off. The reflow is continuous and free.

The same six-card grid in a 900px container showing four columns and a 500px container showing two
Same HTML, same CSS, two containers. Nothing here is a screenshot at two browser widths, it's the identical repeat(auto-fit, minmax(200px, 1fr)) rule rendering inside a 900px and a 500px box side by side, rendered live to confirm the reflow actually happens with zero @media rules.
A wooden wall shelf holding rows of ceramic cups evenly spaced across each shelf
Photo by Curated Lifestyle on Unsplash

auto-fit vs auto-fill: what's the real difference?

This is the part people get wrong, and I did too for an embarrassingly long time. Both keywords create as many tracks as fit. The difference only appears when the row has extra space that no item fills.

/* Items stretch to fill the row */
.grid-fit  { grid-template-columns: repeat(auto-fit,  minmax(280px, 1fr)); }

/* Empty phantom tracks stay, items keep their min size */
.grid-fill { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }

Say your container is 1200px wide and you have two cards. With auto-fill, the grid still reserves the extra column tracks it could fit, leaves them empty at 280px each, and your two cards sit at the left looking lonely. With auto-fit, those empty tracks collapse to zero width, and the 1fr maximum lets your two real cards stretch to share the full 1200px. Per MDN, that collapse is the entire distinction. Which one do you want? Almost always auto-fit, because stretched cards look intentional and empty phantom columns look like a bug. Use auto-fill when you specifically want items to keep a fixed size and not balloon, like a photo grid where thumbnails shouldn't grow past their natural width.

When Should You Reach for auto-fit Instead of auto-fill?

Pick auto-fit for nearly every card grid, gallery, or dashboard layout, because stretched cards read as intentional design while empty phantom columns read as a layout bug. Reach for auto-fill only when items must keep a fixed size no matter how much extra row space exists, a thumbnail strip or a fixed-width tag list where growing past the natural size would look wrong. According to MDN, both keywords generate the same number of tracks given identical container width and minmax values, so switching between them changes zero layout math, only what happens to the leftover space.

Choose auto-fit for nearly every card grid because stretched items look intentional, and reserve auto-fill for the rare case where items must hold a fixed size regardless of leftover row space, such as a thumbnail strip that should never balloon past its natural width.

Share this Post on X Bluesky
auto-fitauto-fill
Empty tracksCollapse to zero widthStay reserved at their minmax floor
Items with extra row spaceStretch to fill via the 1fr maximumStay close to the minimum (282px in a 1200px container), because the empty tracks share the leftover
Visual result on a wide container with few itemsCards stretch, looks intentionalCards stay small, empty phantom columns
Best forCard grids, most layoutsFixed-size thumbnails that shouldn't balloon

What does minmax() actually control?

minmax(min, max) defines a size range for a track, and it's the load-bearing part of this pattern. The minimum sets the point where a column is allowed to drop to the next row. The maximum, usually 1fr, lets the surviving columns divide the space evenly.

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
  gap: 1rem;
}

Notice min(320px, 100%) as the minimum. That's a small guard I add on almost every grid now. A bare minmax(320px, 1fr) can overflow when the container is narrower than 320px, because the track refuses to shrink past its floor. Wrapping the floor in min(320px, 100%) says "320px, unless that's wider than the container, in which case just use 100%." One item on a 300px screen then fits cleanly instead of forcing a horizontal scrollbar. It's four extra characters that kill a whole class of mobile overflow bugs.

Where does clamp() fit in?

clamp(min, preferred, max) gives you a fluid value that scales with the viewport but stays inside bounds. It pairs beautifully with intrinsic grids for the gap, the track floor, or padding, so those scale too instead of jumping.

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(clamp(240px, 30vw, 340px), 1fr));
  gap: clamp(1rem, 2.5vw, 2rem);
}

Here the track minimum grows from 240px on a phone toward 340px on a big monitor, tracking 30vw in between. The gap breathes the same way, from 1rem up to 2rem. No breakpoints touch any of it. Is this overkill for a simple three-card row? Probably. But on content-heavy dashboards, fluid gaps and floors remove that stair-step feeling you get from hard breakpoints, and users never notice the layout "snapping."

How do gap and the grid work together?

gap sets the space between tracks, and the auto-fit math already accounts for it, which surprises people. The browser fits columns using track width plus gap, so you don't subtract anything by hand.

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 24px 16px; /* row-gap 24px, column-gap 16px */
}

You can pass one value for both axes or two for row and column separately. The old margin hacks, negative margins on the container to cancel edge gaps, are gone. gap only applies between items, never on the outer edges, so your grid sits flush with its container. If you've ever fought :last-child margin rules to kill a trailing gutter, you'll appreciate how little there is to think about here. Moving a grid like this into a component's style prop means camelCase keys and quoted values, and the CSS to React style converter writes that object from a pasted rule.

A wall arrangement of framed photos laid out in a loose grid above a side table
Photo by ASR Design Studio on Unsplash

Where Exactly Does the Grid Switch Column Count?

"No breakpoints" is a slight exaggeration. The grid has breakpoints, you just don't write them. They fall wherever the arithmetic says, and I wanted the actual numbers rather than my own back-of-envelope version. So I asked headless Chromium (151.0.7922.34, Playwright 1.62.1, Node 22.23.3, Apple M1, 2026-10-16) to lay out a repeat(auto-fit, minmax(280px, 1fr)) grid of twelve items at every container width from 200px to 1600px, one pixel at a time, and report the resolved track list each time. Nothing here is timed, so it doesn't matter that the machine was busy, and repeated runs printed identical results.

The rule it confirmed, to the pixel, for every gap I tried: n columns first fit at n * minimum + (n - 1) * gap.

Gap2 columns3 columns4 columns5 columns
0px560px840px1120px1400px
16px576px872px1168px1464px
24px584px888px1192px1496px
32px592px904px1216px1528px

If you assumed 560, 840 and 1120 from the 280px floor, your breakpoints are off by the gap, and the error grows with every column. That's 72px at four columns with a 24px gap. It's the reason a container 860px wide, which "should" have three columns by that arithmetic, shows two.

The second thing the sweep showed is how violent the changes are. The card width isn't smooth, it's a sawtooth. With a 24px gap:

Container widthColumnsWidth of one card
583px1583px
584px2280px
887px2431.5px
888px3280px
1191px3381px
1192px4280px

One pixel of container width took a card from 583px to 280px. A card is never narrower than the minimum, but in a single-column grid it can be 2.08 times wider than it, and your card's internal layout, image ratio and line lengths have to survive that whole range. The 1fr maximum isn't a polish detail. It decides how wide the card is for most of the range.

Now the real-page version, because container width isn't viewport width. I put the same grid in a page with 24px of horizontal padding on each side and swept the viewport (an iframe, whose width is its viewport) from 300px to 1700px, twice: once with the overlay scrollbars headless Chromium uses by default, and once with a 15px classic scrollbar. Headless can't draw a native classic scrollbar, so I stood one in with ::-webkit-scrollbar { width: 15px }, which reserves the same 15px.

ColumnsContainer breakpointViewport, overlay scrollbarViewport, 15px classic scrollbar
2584px632px647px
3888px936px951px
41192px1240px1255px
51496px1544px1559px

The overlay column is the container breakpoint plus 48px of padding, exactly. The classic column is 15px later again, exactly. So a visitor on a 1250px window with a Windows-style scrollbar sees three columns while a visitor on 1250px with a trackpad sees four, and the same is true for every 15px slice above each breakpoint. If you screenshot your layout on a laptop and ship it, some of your visitors are looking at a different grid than the one you checked.

The column count of an auto-fit grid changes at n times the minimum plus (n minus 1) gaps. With a 280px minimum and 24px gap that is 584, 888 and 1192px of container width, and a 15px classic scrollbar pushes each viewport breakpoint 15px later.

Share this Post on X Bluesky

And the cases the intro section waves at, measured in a 1200px container with the 24px gap:

Itemsauto-fit, one cardauto-fill, one card
11200px282px
2588px282px
3384px282px
5282px282px

Both modes created the same number of tracks, four, and both reported them in the resolved grid-template-columns. The difference is that auto-fit kept the empty ones at 0px. The 282px under auto-fill isn't the 280px floor either: the empty tracks take their share of the 1fr leftover, so the cards creep above the minimum. At 600px, 900px and 1200px the lone card measured 288, 284 and 282px.

Finally, the overflow case. In a 240px container the bare minmax(280px, 1fr) produced a 280px track, which overflowed by 40px. At 200px the overflow was 80px. With minmax(min(280px, 100%), 1fr) both measured zero. At 280px and above there was no difference at all, so the guard costs nothing where it isn't needed.

Limits. One engine, so Firefox and Safari could round fractional tracks differently, though the breakpoint formula is straight from the spec's track sizing. A classic scrollbar is emulated, not native. And I only varied one minimum (280px), though the formula held for every gap.

Where does this pattern break?

Three failure modes come up often with this pattern, and it's not magic, pretending otherwise will burn you eventually. Knowing which one you're hitting takes about ten seconds if you check in this order:

  1. Check the container width against your minmax floor. If the container is narrower than the floor, that's overflow, fix it with min(floor, 100%).
  2. Count the items in the row. If there's exactly one card and it stretches oddly wide, that's the single-item stretch problem, cap it with max-width or switch to auto-fill.
  3. Check whether the layout issue is inside a single card rather than the column count. If a card itself needs to restructure, that's outside what auto-fit can ever solve, reach for a container query instead.

First, very narrow containers. If your minmax floor is 280px and the grid lives inside a 240px sidebar, the track can't shrink and content overflows. The min(280px, 100%) trick fixes it, but you have to remember to apply it. Second, single-item rows. With one card and auto-fit, that card stretches to the full width, which can look absurd at 1400px. Cap it with a max-width on the item or switch to auto-fill so it keeps a sane size. Third, and this is the real ceiling: auto-fit only changes how many columns exist. It can't restyle the inside of a card. When a card needs to go from a stacked layout to an image-beside-text layout based on its own width, no amount of grid track math helps. That's exactly when a container query beats it, because the query reacts to the item's available space, not the column count. For the broader tradeoff between one-dimensional and two-dimensional layout, see the pillar on CSS Grid vs Flexbox.

auto-fit only changes how many columns exist in a grid; it can't restyle the inside of a card. A is a CSS rule that styles an element based on its own available space rather than the viewport, and that's exactly when it beats auto-fit for component-level responsiveness.

Share this Post on X Bluesky

What Else Should You Read?

  • CSS Container Queries - reach for these when the viewport-based, track-count approach isn't enough and a component needs to restyle based on its own width.

Frequently Asked Questions

What's the difference between auto-fit and auto-fill?
auto-fill keeps empty tracks in the grid when there's leftover space, so items stay at their minimum size and gaps sit at the end of the row. auto-fit collapses those empty tracks to zero width, which lets the existing items stretch to fill the whole row. Per MDN, both create as many tracks as fit; the difference only shows up when the row isn't full.
Does a responsive CSS grid without media queries work in all browsers?
Yes. CSS Grid sits at roughly 95% global browser support in 2026 per caniuse data, and repeat with auto-fit and minmax has shipped in every major browser for years. You don't need a fallback for modern projects, though you can provide a simple block layout for the small slice of older browsers if your audience needs it.
At what width does an auto-fit grid add a column?
A new column appears when the container reaches n times the minimum plus (n-1) gaps. With minmax(280px, 1fr) and a 24px gap, that is 584px for two columns, 888px for three and 1192px for four, which I confirmed pixel by pixel in headless Chromium 151 by sweeping the container from 200px to 1600px. Those are container widths. A page with 24px side padding reaches them at viewport widths 48px larger, and 15px larger again when a classic scrollbar takes space.
Why does my grid overflow on narrow phones?
The minmax minimum is a hard floor. If you write minmax(280px, 1fr) and the container is only 250px wide, one 280px track won't fit and the content overflows horizontally. Fix it by lowering the minimum or using min(280px, 100%) so the track can shrink below 280px when it has to.
When should I reach for a container query instead?
Use a container query when the item itself needs to change layout based on the space it sits in, not just the number of columns. auto-fit only reflows a track count; it can't restyle a card's internals. If a card should switch from stacked to side-by-side at a certain width, a container query is the right tool.