Responsive dashboard grids should usually change their column count before their cards become too narrow. A fixed track list, such as four fractional columns, keeps four columns even when the container no longer has room for four readable cards. The result is cramped content or horizontal overflow.
The implementation uses a repeating Grid track definition with auto-fit and a minimum-to-flexible range. The minimum is 180px, which represents the narrowest acceptable card in this fixture. The flexible maximum allows the tracks that remain on a row to share leftover space evenly.
The important decision is to put the readability threshold in the grid’s track sizing rule. auto-fit asks the browser to create as many tracks as fit. When another minimum-width track plus its gap no longer fits, Grid reduces the number of columns and reflows the same cards into another row. No card-specific breakpoint is needed for that transition.
With six cards, a wide container can resolve to four columns and two rows. At a middle width, it can resolve to two columns and three rows. At the narrow boundary, two 180px tracks and the gap fit at 376px but not at 375px, so the layout becomes one column. The markup and card count stay the same; only the track count changes.
Choose the minimum from the card’s real content, not from a generic device breakpoint. Short labels and values may work in a narrower card than summaries with supporting text, controls, or long localized labels.
This pattern solves an outer-layout problem. It does not make an unbreakable string, an oversized control, or poorly designed card internals shrink correctly. Those need their own content and overflow rules. It is also not the best fit for a strict comparison matrix or table where column alignment must remain fixed across rows.
Use this approach when cards are independent units that can move between rows without changing meaning. It keeps the responsive decision alongside the Grid sizing rule and lets available inline space, rather than a guessed viewport breakpoint, determine when a column disappears.
Working example
export const MIN_CARD_WIDTH = 180; const defaultCards = [ 'Revenue', 'Active users', 'Open tickets', 'Conversion', 'Deployments', 'Uptime', ]; function escapeHtml(value) { return String(value) .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } export function dashboardHtml(cards = defaultCards) { const cardMarkup = cards .map( (title, index) => ` <article class="card" data-card="${index}"> <h2>${escapeHtml(title)}</h2> <p>Dashboard metric ${index + 1}</p> </article>`, ) .join(''); return `<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> * { box-sizing: border-box; } html, body { margin: 0; } body { font-family: system-ui, sans-serif; background: #e2e8f0; } .dashboard { display: grid; grid-template-columns: repeat(auto-fit, minmax(${MIN_CARD_WIDTH}px, 1fr)); gap: 16px; width: 100%; } .card { min-height: 72px; padding: 12px; border: 1px solid #94a3b8; border-radius: 8px; background: white; } h2 { margin: 0; font: 600 16px system-ui, sans-serif; } p { margin: 8px 0 0; font: 14px system-ui, sans-serif; } </style> </head> <body> <main class="dashboard" aria-label="Dashboard cards">${cardMarkup}</main> </body> </html>`; } Read the full article: https://chriseugenerodriguez.com/blog/grid-auto-fit-minmax-dashboard