#React#TypeScript

CSS Grid Mastery: From Two-Dimensional Layout to Practice

Flexbox is one-dimensional; Grid is two-dimensional—real page layout should use Grid. This article explains Grid's core concepts (rows/columns/areas/tracks), the fr unit and minmax, grid-template-areas semantic layout, responsive Grid (auto-fit/auto-fill), and Grid vs Flexbox tradeoffs, taking you from 'able to use Grid' to 'mastering Grid'.

Key Points

  • Topic Focus: React、TypeScript
  • Suitable for: indie hackers, full-stack engineers, AI application developers
  • Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
Author @admin·Published July 26, 2026·Updated July 26, 2026

CSS Grid Mastery: From Two-Dimensional Layout to Practice

"Flexbox lays out a row; Grid lays out a table."

Many developers write React for years and still use Flexbox for everything: display: flex as a one-stop shop—outer flex, inner flex, nested flex inception. Does it work? Yes. Is it elegant? No.

The page skeleton, a typical "two-dimensional layout"—where at the same moment you need to control both rows and columns—written with Flexbox basically becomes a "patchwork wall" of nesting + flex-grow + media queries.

CSS Grid is a layout standard officially landed in 2017. It is the first system in the browser truly designed for two-dimensional layout. Once you build the mental model for Grid, 90% of "responsive layout headaches" simply disappear.

This article does not list APIs; it covers only mental models + practical tradeoffs. After reading you should be able to:

  • Look at a design mock and have grid-template-* pop into your head
  • Know when to use Grid and when to use Flex
  • Write adaptive card grids with zero media queries
  • Use grid-template-areas to write semantic layouts where "one glance at the CSS tells you the page structure"

1. Why Grid: The Essence of Two-Dimensional Layout

The first step in understanding Grid is to set aside the "one-dimensional" intuition of Flexbox.

Flexbox is one-dimensional: it can only arrange items along a single direction (the main axis) at a time. Even if you set flex-wrap: wrap to make it wrap, you cannot control how many items are on the second row or how each row aligns—the browser decides that itself.

Grid is two-dimensional: you can simultaneously explicitly control rows and columns. Each child's position can be precisely described as "row N, column M," and rows and columns are equal and symmetric.

Look at this comparison diagram and you'll understand:

text
Flexbox (1D)                 Grid (2D)
┌──────────────────────┐    ┌─────┬─────┬─────┐
│ [A][B][C]            │    │  A  │  B  │  C  │
┌──────────────────────┐    ├─────┴──┬──┴─────┤
│ [D][E]  ← wrapped,   │    │   D    │   E    │
│         no control   │    │        │        │
└──────────────────────┘    ├────────┴────────┤
                            │       F         │
                            └─────────────────┘
   Everything via main axis      Rows + cols at once

When should you use Grid? In one sentence: whenever your layout is visually "table-shaped" or "card-shaped"—that is, a single row has multiple aligned columns, and there are alignment relationships across rows too—use Grid.

Specific scenarios:

  • Whole-page skeletons (header / sidebar / main / footer)
  • Dashboard card walls
  • Galleries / product lists
  • Two-column form layouts (label | input)
  • Any UI where "rows + columns both need to align"

Conversely, inside components, a row of horizontally arranged buttons, an icon + text—these are Flex's home turf; don't force Grid.


2. Core Properties: grid-template and Tracks

The core of Grid is one set of properties: define the grid structure on the container, and children fall into cells automatically.

2.1 The three most common container properties

css
.grid {
  display: grid;
  grid-template-columns: 200px 1fr 1fr;  /* Three columns: fixed + two flexible shares */
  grid-template-rows: auto 1fr auto;     /* Three rows: content height + flexible + content height */
  gap: 16px;                              /* Row/column gap; finer-grained version is row-gap/column-gap */
}

A core concept appears here: the track. A track is "one whole column" or "one whole row" of the grid. grid-template-columns defines the width sequence of column tracks.

Layout sketch:

text
grid-template-columns: 200px 1fr 1fr;
grid-template-rows:    auto  1fr  auto;

     col1(200px)  col2(1fr)   col3(1fr)
   ┌───────────┬───────────┬───────────┐
r1 │           │           │           │  auto (sized by content)
   ├───────────┼───────────┼───────────┤
r2 │           │           │           │  1fr (eats remaining space)
   │           │           │           │
   ├───────────┼───────────┼───────────┤
r3 │           │           │           │  auto
   └───────────┴───────────┴───────────┘
              gap = 16px

2.2 The fr unit: the soul of Grid

fr (fraction) is the flexible unit introduced by Grid. It represents distributing the remaining space by share.

css
/* Three equal-width columns */
grid-template-columns: 1fr 1fr 1fr;

/* Sidebar + main area, 2:5 ratio */
grid-template-columns: 240px 5fr;     /* 240px fixed, all the rest goes to main */

/* Classic 1:2:1 */
grid-template-columns: 1fr 2fr 1fr;

Key insight: fr distributes remaining space, not total space. If a column's content stretches it, the fixed columns take priority and the fr columns split what's left. This is consistent with Flexbox's flex-grow, but in Grid it's written more intuitively.

2.3 minmax: Grid's true flexible beauty

fr alone isn't enough—1fr collapses to 0 when content is sparse and gets blown wide when content is long. minmax() solves this:

css
/* This column's min is 200px, max is 1fr */
grid-template-columns: minmax(200px, 1fr) 3fr;

minmax(min, max) gives a track an "elastic range": never smaller than min, never larger than max. This is the cornerstone of responsive Grid in the next section.

2.4 repeat(): write less boilerplate

css
/* Don't write */
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;

/* Write */
grid-template-columns: repeat(6, 1fr);

/* Mixed */
grid-template-columns: 200px repeat(3, 1fr) 100px;

repeat(n, size) is syntactic sugar, but combined with auto-fit in the next section, that's where its true power lies.


3. grid-template-areas: Semantic Named Layout

This is, in my opinion, the most underrated feature of Grid. It makes your CSS read like an ASCII-drawn layout sketch.

3.1 Lay out the entire page with names

css
.layout {
  display: grid;
  grid-template-columns: 220px 1fr;
  grid-template-rows: 60px 1fr 50px;
  grid-template-areas:
    "header  header"
    "sidebar main"
    "footer  footer";
  min-height: 100vh;
  gap: 12px;
}

.layout > header  { grid-area: header; }
.layout > aside   { grid-area: sidebar; }
.layout > main    { grid-area: main; }
.layout > footer  { grid-area: footer; }

Look at grid-template-areas—it is the shape of the page. You don't need to look at the HTML; just reading the CSS tells you what this page looks like:

text
┌─────────────────────────────┐
│           header            │
├──────────┬──────────────────┤
│          │                  │
│ sidebar  │       main       │
│          │                  │
├──────────┴──────────────────┤
│           footer            │
└─────────────────────────────┘

Each child needs just one line, grid-area: xxx, to fall into place. No need to calculate row or column numbers, no span, no arithmetic puzzles like grid-column: 1 / 3.

3.2 A few rules to remember

  1. Each name must form a rectangular area (not an L-shape, not a T-shape).
  2. To make a block span multiple columns/rows, repeat the name multiple times (the header spanning two columns above is written exactly this way).
  3. Use . to indicate "this cell is empty."
  4. The number of cells in each row must be equal.

3.3 Rearrange layout with media queries

This is the most satisfying use of grid-template-areas—responsive rearrangement requires only changing a string block, and the HTML doesn't move at all:

css
/* PC: left sidebar */
@media (min-width: 768px) {
  .layout {
    grid-template-columns: 220px 1fr;
    grid-template-areas:
      "header  header"
      "sidebar main"
      "footer  footer";
  }
}

/* Mobile: sidebar sinks to the bottom */
@media (max-width: 767px) {
  .layout {
    grid-template-columns: 1fr;
    grid-template-areas:
      "header"
      "main"
      "sidebar"
      "footer";
  }
}

In JSX it's just:

tsx
<div className="layout">
  <header />
  <aside />
  <main />
  <footer />
</div>

This is the Grid usage I recommend most often to teams in projects. Its readability crushes any row-number calculation.


4. Responsive Grid: auto-fit / auto-fill + minmax

The next section is the most "magical" part of Grid—write one line of CSS and produce a media-query-free adaptive card wall.

4.1 Adaptive card grid: one line of CSS

css
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 16px;
}

Effects of this line:

  • Wide viewport → automatically fit more cards, each at least 240px
  • Narrow viewport → automatically reduce the column count, each still 240px or more
  • No media query needed at all

Sketch (viewport from wide to narrow):

text
Viewport 1200px: each 240px+, 4 columns
┌────┬────┬────┬────┐
│ C1 │ C2 │ C3 │ C4 │
└────┴────┴────┴────┘

Viewport 800px: 3 columns
┌─────┬─────┬─────┐
│ C1  │ C2  │ C3  │
└─────┴─────┴─────┘

Viewport 500px: 2 columns
┌────────┬────────┐
│   C1   │   C2   │
└────────┴────────┘

Principle breakdown:

  • minmax(240px, 1fr): each column min 240px, max eats remaining space
  • auto-fit: the browser computes how many columns fit and packs the container
  • repeat(): repeats the above recipe N times, where N is computed by the browser

4.2 auto-fit vs auto-fill: one key difference

These two values behave the same 90% of the time; only when the card count is fewer than the number of columns that could fit does the difference emerge:

text
Container can fit 4 columns, but only 2 cards

auto-fill:                       auto-fit:
┌────┬────┬────┬────┐            ┌──────────┬──────────┐
│ C1 │ C2 │    │    │            │    C1    │    C2    │
└────┴────┴────┴────┘            └──────────┴──────────┘
Keeps empty track slots           Collapses empty tracks, stretches existing cards
  • auto-fill: always keeps track placeholders; cards line up "left-aligned," leaving the rest empty.
  • auto-fit: collapses empty tracks to 0, letting existing cards stretch to fill the entire container.

Practical experience: 99% of card-list scenarios (galleries, products, blogs) use auto-fit—when there are few cards, letting them stretch to fill looks less "deflated." auto-fill is better suited for "placeholder waiting to be filled" scenarios, like fixed cells in category navigation.

4.3 Why this beats media queries

The pain of the classic media-query approach:

css
.cards { grid-template-columns: 1fr; }
@media (min-width: 540px) { .cards { grid-template-columns: 1fr 1fr; } }
@media (min-width: 800px) { .cards { grid-template-columns: 1fr 1fr 1fr; } }
@media (min-width: 1080px) { .cards { grid-template-columns: repeat(4, 1fr); } }

Four breakpoints, and you have to tweak them every time the window size changes. minmax + auto-fit does it in one line, and the response is continuous, not stepped.


5. Placing Children: span / line numbers / named lines

After the container defines the grid, by default children fall into cells one per cell in order. But the real power of Grid is that each child can precisely declare where it sits and how many cells it spans.

5.1 Three placement methods

Method one: grid line numbers

Grid automatically numbers each line, starting from 1. Column lines run left to right 1, 2, 3..., and row lines run top to bottom 1, 2, 3...

css
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}

.item-a { grid-column: 1 / 3; }   /* From line 1 to line 3, spans 2 columns */
.item-b { grid-row: 2 / 4; }      /* From line 2 to line 4, spans 2 rows */
text
       1     2     3     4
    ┌─────┬─────┬─────┐
  1 │  A        │     │
    ├─────┼─────┼─────┤
  2 │     │     │     │
    │  B            │ │  ← B from row line 2 to row line 4
  3 │     │     │     │
    └─────┴─────┴─────┘
  4

/ is the start line, after / is the end line. The end is a line, not a cell, so 1 / 3 occupies cells 1 and 2 (the area between two lines is one cell).

Method two: using span (number of cells to span)

Don't want to calculate the end line? Use span:

css
.hero { grid-column: span 2; }     /* Spans 2 columns */
.banner { grid-column: 1 / span 3; } /* From line 1, spans 3 columns */
.tall { grid-row: span 4; }        /* Spans 4 rows */

This is the most common pattern in dashboards—large cards span 2 cells, small cards span 1 cell.

Method three: named grid lines

In grid-template-*, use [name] to tag a line:

css
.layout {
  grid-template-columns: [sidebar-start] 200px [sidebar-end main-start] 1fr [main-end];
}

.sidebar { grid-column: sidebar-start / sidebar-end; }
.main    { grid-column: main-start / main-end; }

Far more readable than pure numbers, and combined with grid-template-areas you can achieve "zero numbers across the board."

5.2 z-order and stacking

Grid cells can overlap—multiple children placed in the same cell, with z-index deciding who's on top. This means you can use Grid for overlay-style layouts (like a hero image with a mask):

css
.hero {
  display: grid;
}
.hero img       { grid-area: 1 / 1; }    /* Occupies the whole block */
.hero .overlay  { grid-area: 1 / 1; }    /* Same cell, layered on top */
.hero .cta      { grid-area: 1 / 1; align-self: end; }

All three elements crammed into 1 / 1, naturally stacked. This is one of the areas where Grid is more flexible than Flex.


6. Grid vs Flexbox: When to Use Which

This is the question newcomers ask most. One-line principle:

Page skeleton uses Grid, inside components use Flex.

Finer distinctions:

ScenarioUseWhy
Whole page / large block layoutGrid2D, controls rows + cols at once
Dashboard card wallGridauto-fit + minmax in one line
Form label/input two-columnGridStrictly aligned two columns
A row of buttons / nav itemsFlex1D horizontal arrangement
Icon + textFlexContent-driven, doesn't need alignment to outer columns
Centering one elementFlex (one line)Grid works too, but Flex is lighter
Text truncation + trailing buttonFlexmin-width: 0 plays nicer with flex

Mental model:

  • Flex thinks: "line these things up along a line; who gets more, who gets less, is decided by flex-grow."
  • Grid thinks: "the whole area is a chessboard; each thing is placed at row N, column M."

They are not mutually exclusive—in real projects it's usually an outer Grid container, with each Grid child using Flex internally to lay out its own small content. The two layers have clear responsibilities; there's no "either/or."

Example:

tsx
<div className="dashboard">       {/* Grid: defines the card wall */}
  <div className="card">
    <div className="card-head">   {/* Flex: title + action button in a row */}
      <h3 />
      <button />
    </div>
    <div className="card-stats">  {/* Flex: 3 numbers in a horizontal row */}
      <span />
      <span />
      <span />
    </div>
  </div>
</div>

The outer Grid lays out the card wall; the inner Flex handles the row of content inside each card. This is the correct posture in production.


7. Three Practical Examples

7.1 Holy Grail Layout

A classic among classics: header, footer, and three middle columns (left nav, center main, right aside).

css
.holy-grail {
  display: grid;
  grid-template-columns: 200px 1fr 240px;
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    "header header header"
    "nav    main   aside"
    "footer footer footer";
  min-height: 100vh;
  gap: 16px;
}

.holy-grail > header { grid-area: header; }
.holy-grail > nav    { grid-area: nav; }
.holy-grail > main   { grid-area: main; }
.holy-grail > aside  { grid-area: aside; }
.holy-grail > footer { grid-area: footer; }

/* Mobile collapse */
@media (max-width: 768px) {
  .holy-grail {
    grid-template-columns: 1fr;
    grid-template-areas:
      "header"
      "main"
      "nav"
      "aside"
      "footer";
  }
}

JSX:

tsx
<div className="holy-grail">
  <header />
  <nav />
  <main />
  <aside />
  <footer />
</div>

Compare writing the same layout with Flexbox—you need nesting, order, media queries that change flex-direction, plus a pile of flex-basis. The Grid version is half as long and an order of magnitude more readable.

7.2 Dashboard grid (uneven cards)

A typical dashboard need: a few large cards (KPI trend charts), a few small cards (numbers, statuses), all pieced into a wall.

css
.dashboard {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: 140px;   /* Each row defaults to 140px tall */
  gap: 16px;
}

.kpi-big   { grid-column: span 2; grid-row: span 2; }   /* 2x2 large card */
.kpi-wide  { grid-column: span 2; }                     /* Wide card */
.kpi-tall  { grid-row: span 2; }                        /* Tall card */
.kpi-small { /* default 1x1 */ }

Effect sketch:

text
┌──────────────┬──────────────┬──────┬──────┐
│              │              │  S1  │  S2  │
│   kpi-big    │   kpi-wide   ├──────┴──────┤
│   (2x2)      │              │    kpi-tall │
│              ├──────┬───────┤    (1x2)    │
├──────────────┤  S3  │  S4   │             │
│   kpi-wide   │      │       │             │
└──────────────┴──────┴───────┴─────────────┘

Key technique: grid-auto-rows: 140px makes each row a fixed height, so a span 2 card is 140 * 2 + gap tall, and cards that span rows align naturally. This is the core trick of dashboard layouts.

7.3 Adaptive card list (the responsive ace)

The most common, most practical one—blog post lists, product walls, image galleries all rely on it:

css
.card-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 20px;
}

.card {
  /* Inside the card, use Flex to handle vertical content */
  display: flex;
  flex-direction: column;
  padding: 20px;
  border-radius: 8px;
}

.card-body {
  flex: 1;   /* Makes all card bottoms align */
}
tsx
export function ArticleList({ articles }: { articles: Article[] }) {
  return (
    <section className="card-list">
      {articles.map(a => (
        <article key={a.id} className="card">
          <h3>{a.title}</h3>
          <p className="card-body">{a.excerpt}</p>
          <a href={`/a/${a.slug}`}>Read full article</a>
        </article>
      ))}
    </section>
  );
}

Here's an advanced trick: use flex: 1 to stretch .card-body. This way, no matter how long a card's title/excerpt is, the "Read full article" button always aligns at the bottom of the card—visually uniform.

text
Same-row cards, varying title lengths:

┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│ Short title  │  │ This is a    │  │ Medium title │
│              │  │ very long    │  │              │
│              │  │ title        │  │              │
│ Summary...   │  │ Summary...   │  │ Summary...   │
│              │  │              │  │              │
│              │  │              │  │              │
│ → Read full  │  │ → Read full  │  │ → Read full  │  ← all aligned
└──────────────┘  └──────────────┘  └──────────────┘

This is the classic pairing of outer Grid + inner Flex—Grid manages macro layout, Flex manages micro content.

7.4 Bonus: CSS subgrid

If you need nested child grids to share the column tracks of the parent grid (e.g., table-like multi-column cards), traditional CSS can't do it—once a child enters a Grid container it forms its own independent grid. subgrid solves this:

css
.parent {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}

.child {
  display: grid;
  grid-template-columns: subgrid;   /* Inherits the parent grid's column definition */
  grid-column: span 3;
}

As of 2024, all major browsers (Chrome 117+, Safari 16+, Firefox 71+) fully support it. You can use it confidently in production—it solves the "nested table card" pain point that Grid previously had.


Summary

Returning to the judgment at the start of this article, having now walked through Grid's full picture, here are three key points:

  1. Grid is a two-dimensional system; Flex is a one-dimensional system. Layouts where "both rows and columns matter"—page skeletons, card walls, aligned tables—should prefer Grid. Content-driven one-dimensional arrangements like rows of buttons and icon+text belong to Flex.

  2. minmax(min, 1fr) + auto-fit is the nuclear weapon of responsive layout. One line of CSS implements "cards adapt their column count to the viewport, with no media queries." 99% of card-list scenarios use auto-fit (not auto-fill)—it lets cards fill the container when there are few of them.

  3. grid-template-areas lets CSS explain the page structure itself. Named areas, ASCII-style layout declarations, and rearranging by swapping a string block in a media query—this is Grid's most underrated and most team-worthy feature.

Three pieces of practical experience:

  • If you can use span, don't compute line numbers. grid-column: span 2 is more readable and easier to maintain than grid-column: 1 / 3.
  • For cards that span rows, use grid-auto-rows to fix row height. The secret of dashboard alignment in one line.
  • Outer Grid, inner Flex. Big structures are laid out with Grid, small content arranged with Flex—responsibilities divided, no conflicts.

Golden line: Flexbox solves "how to arrange this row," Grid solves "how to lay out this page." Treat them as two different tools, not one as a substitute for the other—your CSS will instantly become a notch cleaner.

In the next article we'll talk about Container Queries—element-level responsiveness, which will once again overturn your habit of writing media queries.

Related

Related Content