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.

· · 6 min read
Responsive CSS grid layout code in a dark-theme editor

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.

Quick take: A responsive CSS grid without media queries works by handing track sizing to the grid itself. The one-liner grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)) fits as many columns as the container allows and reflows down to a single column on small screens. No breakpoints, no JavaScript, one declaration.

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. I've shipped card grids this way that went 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?

Let's take that value apart piece by piece. It reads like a spell until you see what each token does.

.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.

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.

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.

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

Where does this pattern break?

It's not magic, and pretending it is will burn you. Three failure modes come up often.

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.

  • 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 has roughly 97%+ global browser support in 2026 per MDN and 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 ancient browsers if your audience needs it.
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.