Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

CSS  Module 11 – CSS Grid

Grid fundamentals: tracks, fr and placement

Build two-dimensional layouts with CSS Grid: size rows and columns with fr, place items by line numbers and spans, and see how the implicit grid fills in.

  • Beginner
  • 30 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free: 30 minutes

What you will learn

  • Define rows and columns with grid-template-columns, grid-template-rows and the fr unit
  • Use line numbers and spans, including negative line numbers, to place items
  • Explain the implicit grid, grid-auto-flow and dense packing

Before you start

What you’ll learn · Before you start

What you will learn

  • Define rows and columns with grid-template-columns, grid-template-rows and the fr unit
  • Use line numbers and spans, including negative line numbers, to place items
  • Explain the implicit grid, grid-auto-flow and dense packing

Before you start

On this page

CSS Grid lays out the children of a box in rows and columns at once. The container defines the tracks, rows and columns with sizes such as 200px, auto or 1fr, and numbers the lines between them, so an item can be placed from one line to another: grid-column: 1 / -1 runs across every column. The fr unit shares out whatever space the fixed tracks and the gaps leave over. Items you do not place go into the next free cells, and the grid adds rows when it runs out of them. Flexbox arranges items along one line and lets each line size itself; grid lines items up in both directions. Grid Layout Levels 1 and 2 are reliable Candidate Recommendations in CSS Snapshot 2026, and grid is Baseline widely available, so every current browser supports it. The numbers below come from Chromium 153, Firefox 155 and WebKit 26.6, measured on the same pages.

Containers, tracks, lines and cells

display: grid makes an element a grid container, and each of its children a grid item. Only the children: their own children are laid out as usual inside them, unless they become grids too (the subgrid lesson of this module shares one grid across levels). The container’s grid-template-columns and grid-template-rows list the tracks, which together make up the explicit grid.

A 3-column, 2-row grid with its lines numbered 1 to 4 from the start and -4 to -1 from the end, a cell and an area of two cells.3 columns, 2 rows, 20 px gapsgrid-template-columns: 1fr 1fr 1fr1234-4-3-2-1123-3-2-1a cellrow 1, col 1an area of 2 cellsgrid-column: 2 / -1Lines are numbered from 1 (top, blue) and from -1at the end (bottom, grey). A gap sits on a line:line 2 is the 20 px gap between columns 1 and 2.

Lines, tracks, cells and areas of a small grid

Text description of the diagram

A grid of three equal columns and two rows, with 20 px gaps between the tracks, drawn with dashed lines along its grid lines.

  1. The column lines are numbered 1, 2, 3 and 4 from the left along the top, and -4, -3, -2 and -1 along the bottom, so line 4 and line -1 are the same line, the last one.
  2. The row lines are numbered 1, 2 and 3 from the top on the left, and -3, -2 and -1 on the right.
  3. A gap sits on a line: line 2 is the 20 px gap between column 1 and column 2, and row line 2 is the gap between the two rows.
  4. The cell in row 1, column 1 is highlighted: one row track crossing one column track.
  5. In row 2, columns 2 and 3 are highlighted together as one area of two cells, which is what grid-column: 2 / -1 gives an item placed in that row. The other cells are empty.

The words that matter, all visible in the figure:

  • A track is one row or one column. A cell is where one row crosses one column, and a grid area is any rectangle of cells.
  • Lines separate the tracks. They are numbered from 1 at the start edge, and from -1 at the end edge of the explicit grid, so a grid of three columns has column lines 1 to 4, and line 4 is also line -1.
  • A gap (row-gap, column-gap, or both at once with gap) is space between tracks. It sits on a line, never on the outer edges, and items do not go into it.

Sizing tracks: lengths, auto and fr

A track list is a list of sizes, one per track. Six of them, each in a grid 1000 px wide with 20 px between the columns:

Six track lists in a 1000 px grid HTML · tracks.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Six track lists in a 1000 px grid</title>
  <style>
    body { margin: 0; font: 16px/24px Arial, sans-serif; }
    .grid { display: grid; inline-size: 1000px; column-gap: 20px; margin-block-end: 8px; }
    .grid > * { background: #dde; }

    #fixed-and-fr  { grid-template-columns: 200px 1fr 2fr; }
    #equal-fr      { grid-template-columns: 1fr 1fr 1fr; }
    #auto-and-fr   { grid-template-columns: auto 1fr; }
    #auto-only     { grid-template-columns: auto auto; }
    #half-fr       { grid-template-columns: 0.5fr; }
    #percentages   { grid-template-columns: 25% 50% 25%; }
  </style>
</head>
<body>
  <div class="grid" id="fixed-and-fr"><div>200px</div><div>1fr</div><div>2fr</div></div>
  <div class="grid" id="equal-fr"><div>1fr</div><div>1fr</div><div>1fr</div></div>
  <div class="grid" id="auto-and-fr"><div>Menu</div><div>1fr</div></div>
  <div class="grid" id="auto-only"><div>Menu</div><div>Search the shop</div></div>
  <div class="grid" id="half-fr"><div>0.5fr</div></div>
  <div class="grid" id="percentages"><div>25%</div><div>50%</div><div>25%</div></div>
  <script>
    // The used size of every column, as getComputedStyle reports it for a grid container, and how far the
    // columns and gaps reach.
    for (const grid of document.querySelectorAll('.grid')) {
      const right = Math.max(...[...grid.children].map((item) => item.getBoundingClientRect().right));
      console.log(`${grid.id}: ${getComputedStyle(grid).gridTemplateColumns} (columns and gaps end at ${right}px)`);
    }
  </script>
</body>
</html>
grid-template-columns Column widths Columns and gaps reach
200px 1fr 2fr 200 · 253.33 · 506.67 px 1000 px
1fr 1fr 1fr 320 · 320 · 320 px 1000 px
auto 1fr 40.03 · 939.97 px 1000 px
auto auto 451.75 · 528.25 px 1000 px
0.5fr 500 px 500 px
25% 50% 25% 250 · 500 · 250 px 1040 px

The values are what getComputedStyle() reported, which for a grid container is the size each track ended up with. The three engines agreed to within a hundredth of a pixel; the last decimals differ (Chromium measured 253.328 px where Firefox measured 253.333) because each engine stores lengths in its own small fraction of a pixel.

Each row shows a rule:

  • fr shares out the leftover space. The browser takes the fixed tracks and the gaps away first: 1000 − 200 − 2 × 20 leaves 760 px, and 1fr 2fr divides it into thirds, 253.33 and 506.67 px. Because the gaps come off first, fr tracks share only the space there is; they overflow only when their content cannot shrink, a case covered below.
  • auto fits the content when fr tracks are present. “Menu” in 16 px Arial is 40.03 px wide, so the auto column took exactly that and 1fr the rest.
  • auto tracks stretch when nothing else claims the space. With two auto columns and no fr, each got its content’s width (“Menu” 40.03 px, “Search the shop” 116.53 px) plus an equal share of the 823.44 px left over.
  • A total of fr factors below 1 claims only part of the space. A lone 0.5fr column got half the space and left the other half empty. The specification asks for factors of 1 or more unless that partial fill is what you want.
  • Percentages ignore the gaps. 25 %, 50 % and 25 % are 1000 px of tracks, and the two gaps add 40 px more, so the grid overflowed its container in all three engines. 1fr 2fr 1fr gives the same proportions and fits.

repeat(3, 1fr) is short for 1fr 1fr 1fr; repeat(12, 1fr) is how the 12-column layouts below are written. The rule for fr, as a program:

Fixed tracks, gaps and fr shares JavaScript · fr_sizes.mjs
// How a grid shares out space between fixed tracks and fr tracks (CSS Grid Level 2, "Find the Size of an fr"):
// take the fixed tracks and the gaps away from the width, divide what is left by the sum of the fr factors (never by
// less than 1), and give each fr track its factor times that. Percentages are fixed sizes worked out from the width.

function columns(width, tracks, gap = 0) {
  const fixed = tracks.filter((t) => !t.endsWith('fr'));
  const flexible = tracks.filter((t) => t.endsWith('fr'));
  const px = (t) => (t.endsWith('%') ? (parseFloat(t) / 100) * width : parseFloat(t));
  const leftover = width - fixed.reduce((sum, t) => sum + px(t), 0) - gap * (tracks.length - 1);
  const factors = flexible.reduce((sum, t) => sum + parseFloat(t), 0);
  const frSize = Math.max(leftover, 0) / Math.max(factors, 1);
  return tracks.map((t) => (t.endsWith('fr') ? parseFloat(t) * frSize : px(t)));
}

const round = (n) => Math.round(n * 100) / 100;
for (const [tracks, gap] of [
  [['200px', '1fr', '2fr'], 20],
  [['1fr', '1fr', '1fr'], 20],
  [['0.5fr'], 0],
  [['25%', '50%', '25%'], 20],
]) {
  const sizes = columns(1000, tracks, gap);
  const reach = sizes.reduce((sum, s) => sum + s, 0) + gap * (tracks.length - 1);
  const note = reach > 1000 ? `${round(reach - 1000)}px wider than the grid` : reach < 1000 ? `${round(1000 - reach)}px left empty` : 'fills the grid';
  console.log(`${tracks.join(' ').padEnd(13)} gap ${String(gap).padStart(2)}px -> ${sizes.map(round).join(' | ')} (${note})`);
}

Output

200px 1fr 2fr gap 20px -> 200 | 253.33 | 506.67 (fills the grid)
1fr 1fr 1fr   gap 20px -> 320 | 320 | 320 (fills the grid)
0.5fr         gap  0px -> 500 (500px left empty)
25% 50% 25%   gap 20px -> 250 | 500 | 250 (40px wider than the grid)

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node fr_sizes.mjs

One caution before you rely on 1fr for equal columns: on its own it means “an equal share, but never narrower than the content”, so a long word or URL can widen its column. The responsive grids lesson of this module reproduces that and fixes it.

CSS Grid Generator Type track sizes such as 1fr, auto or minmax(200px, 1fr) and see the size each column resolves to, next to the CSS.

Placing items by line numbers

An item is placed by naming the lines it starts and ends at. grid-column: 2 / 4 is short for grid-column-start: 2 and grid-column-end: 4; grid-row does the same for rows. The end can be a span instead (4 / span 3 covers three columns from line 4), and negative numbers count from the end of the explicit grid. A 12-column page:

A 12-column page placed by line numbers HTML · placement.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>A 12-column page placed by line numbers</title>
  <style>
    body { margin: 0; font: 16px/24px Arial, sans-serif; }
    .page {
      display: grid;
      inline-size: 1280px;
      grid-template-columns: repeat(12, 1fr);
      grid-template-rows: 64px 160px 200px 64px;
      gap: 24px;
    }
    .page > * { background: #dde; }

    .header  { grid-column: 1 / -1; }            /* line 1 to the last line: all 12 columns */
    .sidebar { grid-column: 1 / 4; grid-row: 2 / 4; }
    .main    { grid-column: 4 / -1; }
    .card-1  { grid-column: 4 / span 3; }
    .card-2  { grid-column: 7 / span 3; }
    .card-3  { grid-column: -4 / -1; }            /* the last three columns, counted from the end */
    .footer  { grid-column: 1 / -1; }
  </style>
</head>
<body>
  <div class="page">
    <header class="header">Header</header>
    <nav class="sidebar">Sidebar</nav>
    <main class="main">Main</main>
    <article class="card-1">Card 1</article>
    <article class="card-2">Card 2</article>
    <article class="card-3">Card 3</article>
    <footer class="footer">Footer</footer>
  </div>
  <script>
    // Where each item was drawn: its left edge (x), width, top edge (y) and height, in px.
    const round = (n) => Math.round(n * 100) / 100;
    for (const item of document.querySelector('.page').children) {
      const r = item.getBoundingClientRect();
      console.log(`${item.className}: x ${round(r.x)}, width ${round(r.width)}, y ${round(r.y)}, height ${round(r.height)}`);
    }
  </script>
</body>
</html>

The rectangles the three engines drew were identical, to the pixel:

Item grid-column Left, width
Header 1 / -1 0, 1280 px
Sidebar 1 / 4 0, 302 px
Main 4 / -1 326, 954 px
Card 1 4 / span 3 326, 302 px
Card 2 7 / span 3 652, 302 px
Card 3 -4 / -1 978, 302 px
Footer 1 / -1 0, 1280 px

The arithmetic is short. Eleven gaps of 24 px leave 1016 px for twelve columns, 84.67 px each, so three columns and the two gaps inside them make 302 px, and an item starting at line 4 begins after three columns and three gaps, at 326 px. The sidebar also has grid-row: 2 / 4, so it spans two rows, 160 + 24 + 200 = 384 px tall, which is how one item sits beside two others. Card 3 shows why negative lines help: -4 / -1 means “the last three columns” whatever the column count, so the rule keeps working if the page grows to 16 columns.

Two rules handle odd values. When the start line comes after the end line, they are swapped, so 6 / 2 covers lines 2 to 6. When both are the same line, the end is dropped and the item takes one track. Items placed on the same cells overlap, which the alignment lesson of this module uses on purpose.

Layout Grid Calculator Check column and gutter widths for a 12-column page at any container width before you write the track list.

The explicit grid and the implicit grid

The tracks you list are the explicit grid. When there are more items than cells, or an item is placed past the last line, the browser adds implicit tracks to hold them, sized by grid-auto-rows and grid-auto-columns (auto unless you say otherwise). Two grids with three explicit columns and no rows at all:

The explicit grid and the implicit grid HTML · implicit.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>The explicit grid and the implicit grid</title>
  <style>
    body { margin: 0; font: 16px/24px Arial, sans-serif; }
    .grid {
      display: grid;
      inline-size: 600px;
      grid-template-columns: repeat(3, 1fr);   /* the explicit grid: 3 columns, no rows */
      grid-auto-rows: minmax(80px, auto);      /* the size of rows the browser adds */
      grid-auto-columns: 60px;                 /* the size of columns the browser adds */
      gap: 10px;
      margin-block-end: 24px;
    }
    .grid > * { background: #dde; }

    #beyond .far  { grid-column: 5; }          /* past the last explicit line: columns 4 and 5 are added */
    #beyond .wide { grid-column: 1 / -1; }     /* -1 is the last explicit line, not the last line */
  </style>
</head>
<body>
  <div class="grid" id="seven-items">
    <div>1</div><div>2</div><div>3</div><div>4</div><div>5</div><div>6</div><div>7</div>
  </div>
  <div class="grid" id="beyond">
    <div class="far">far</div><div class="wide">wide</div>
  </div>
  <script>
    const round = (n) => Math.round(n * 100) / 100;
    for (const grid of document.querySelectorAll('.grid')) {
      const style = getComputedStyle(grid);
      console.log(`${grid.id}: columns ${style.gridTemplateColumns} | rows ${style.gridTemplateRows}`);
      const box = grid.getBoundingClientRect();
      for (const item of grid.children) {
        const r = item.getBoundingClientRect();
        console.log(`  item ${item.textContent}: x ${round(r.x - box.x)}, width ${round(r.width)}, y ${round(r.y - box.y)}, height ${round(r.height)}`);
      }
      console.log(`  the grid is ${round(box.width)}px wide; its items reach ${round(Math.max(...[...grid.children].map((i) => i.getBoundingClientRect().right)) - box.x)}px`);
    }
  </script>
</body>
</html>

What all three engines did:

  • Seven items, three columns. The browser added three rows, each at least 80 px tall as minmax(80px, auto) asks; the seventh item started a third row on its own. getComputedStyle() listed those implicit rows too: 80px 80px 80px.
  • An item placed at column 5. The browser added columns 4 and 5 at the 60 px of grid-auto-columns, and the three explicit 1fr columns shrank to 146.67 px to make room: 600 − 2 × 60 − 4 × 10 = 440 px, split three ways.
  • grid-column: 1 / -1 beside implicit columns. The second item covered only the three explicit columns, 460 px, because -1 is the last line of the explicit grid, not the last line there is.
  • The second item went to row 2, not row 1. Row 1 had three empty cells, yet the item started a new row. Placement works through the items in order with a cursor that only moves forward: the first item left it at column 5, and an item that must start at column 1 can only come after that, in the next row.

The lesson to keep: set grid-auto-rows whenever the number of items can change, such as a product list, so that new rows get a sensible size, and use negative lines only for the explicit grid you defined.

Auto-placement: sparse and dense

Items without a placement are put in the next cell that can hold them. grid-auto-flow: row, the default, fills row by row; column fills column by column. Both are sparse by default: the placement cursor never moves back, so a wide item that does not fit at the end of a row leaves a hole there. Adding dense sends each item back to the first hole it fits. Six tiles in four columns, B and E two columns wide and C three:

Sparse and dense auto-placement HTML · auto-flow.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Sparse and dense auto-placement</title>
  <style>
    body { margin: 0; font: 16px/24px Arial, sans-serif; }
    .tiles {
      display: grid;
      inline-size: 400px;
      grid-template-columns: repeat(4, 1fr);
      grid-auto-rows: 60px;
      margin-block-end: 24px;
    }
    .tiles > button { font: inherit; border: 1px solid #fff; background: #dde; }
    .span-2 { grid-column: span 2; }
    .span-3 { grid-column: span 3; }

    #dense { grid-auto-flow: row dense; }       /* go back and fill holes; the default is row (sparse) */
    #dense-reading-flow {
      grid-auto-flow: row dense;
      reading-flow: grid-rows;                   /* Tab follows the rows on screen (Chromium 137+ only) */
    }
  </style>
</head>
<body>
  <div class="tiles" id="sparse">
    <button>A</button><button class="span-2">B</button><button class="span-3">C</button>
    <button>D</button><button class="span-2">E</button><button>F</button>
  </div>
  <div class="tiles" id="dense">
    <button>A</button><button class="span-2">B</button><button class="span-3">C</button>
    <button>D</button><button class="span-2">E</button><button>F</button>
  </div>
  <div class="tiles" id="dense-reading-flow">
    <button>A</button><button class="span-2">B</button><button class="span-3">C</button>
    <button>D</button><button class="span-2">E</button><button>F</button>
  </div>
  <script>
    // The row and the columns each tile landed in (from its position: 100px columns, 60px rows), then the order a
    // reader sees them in: top to bottom, left to right.
    for (const grid of document.querySelectorAll('.tiles')) {
      const box = grid.getBoundingClientRect();
      const placed = [...grid.children].map((tile) => {
        const r = tile.getBoundingClientRect();
        const row = Math.round((r.y - box.y) / 60) + 1;
        const first = Math.round((r.x - box.x) / 100) + 1;
        const last = Math.round((r.right - box.x) / 100);
        return { name: tile.textContent, row, first, last };
      });
      console.log(`${grid.id}: ${placed.map((t) => `${t.name} row ${t.row} col ${t.first === t.last ? t.first : `${t.first}-${t.last}`}`).join(', ')}`);
      const seen = [...placed].sort((a, b) => a.row - b.row || a.first - b.first).map((t) => t.name).join(' ');
      console.log(`  order on screen: ${seen}`);
    }
  </script>
</body>
</html>

The same algorithm as a program, which prints the grid it builds:

Placing the six tiles, sparse and dense JavaScript · auto_place.mjs
// Grid auto-placement in rows (CSS Grid Level 2, "Grid Item Placement Algorithm"), for tiles that span columns.
// Sparse, the default: a cursor walks forward through the grid and never goes back, so a tile that does not fit at
// the end of a row leaves a hole there. Dense: every tile starts its search at the first row again, so a later,
// smaller tile can fill an earlier hole, and the order on screen stops matching the order of the HTML.

function place(tiles, columns, dense) {
  const rows = [];                                    // rows[r][c] = the tile in that cell, or undefined
  const free = (r, c, span) => c + span <= columns && [...Array(span).keys()].every((i) => !rows[r]?.[c + i]);
  let cursor = { r: 0, c: 0 };
  for (const tile of tiles) {
    let { r, c } = dense ? { r: 0, c: 0 } : cursor;
    while (!free(r, c, tile.span)) {
      c++;
      if (c + tile.span > columns) { c = 0; r++; }
    }
    rows[r] ??= [];
    for (let i = 0; i < tile.span; i++) rows[r][c + i] = tile.name;
    cursor = { r, c: c + tile.span };
  }
  return rows;
}

const tiles = [
  { name: 'A', span: 1 }, { name: 'B', span: 2 }, { name: 'C', span: 3 },
  { name: 'D', span: 1 }, { name: 'E', span: 2 }, { name: 'F', span: 1 },
];
for (const dense of [false, true]) {
  const rows = place(tiles, 4, dense);
  console.log(dense ? 'grid-auto-flow: row dense' : 'grid-auto-flow: row (sparse)');
  for (const row of rows) console.log('  ' + [...Array(4).keys()].map((c) => row[c] ?? '.').join(' '));
  const order = [...new Set(rows.flat().filter(Boolean))].join(' ');
  const cells = rows.flatMap((row) => [...Array(4).keys()].map((c) => row[c]));
  const holes = cells.slice(0, cells.findLastIndex(Boolean)).filter((cell) => !cell).length;
  console.log(`  on screen: ${order} · empty cells before the last tile: ${holes}`);
}

Output

grid-auto-flow: row (sparse)
  A B B .
  C C C D
  E E F .
  on screen: A B C D E F · empty cells before the last tile: 1
grid-auto-flow: row dense
  A B B D
  C C C F
  E E . .
  on screen: A B D C F E · empty cells before the last tile: 0

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node auto_place.mjs

All three engines placed the tiles exactly as the program does. Then the tiles were made buttons and Tab was pressed six times in each engine, starting just before the grid (Option-Tab in WebKit, which is how Safari reaches buttons):

  • Sparse (row): drawn A B C D E F; Tab went A B C D E F in all three engines.
  • Dense (row dense): drawn A B D C F E; Tab still went A B C D E F in all three engines.
  • Dense with reading-flow: grid-rows: drawn A B D C F E; Tab went A B D C F E in Chromium, and A B C D E F in Firefox and WebKit.

Dense packing moved D in front of C on screen, but keyboard focus and screen readers follow the HTML, so a keyboard user jumped from B down to C and back up to D. WCAG 2.2 asks for a meaningful reading sequence (success criterion 1.3.2) and a focus order that keeps meaning and operability (2.4.3), and the Grid specification says the same in its own words: placement is for visual, not logical, reordering. The reading-flow property is designed to make Tab follow the screen, but only Chromium supports it (Chrome 137 and later), so it cannot be the fix for everyone yet. Use dense packing for collections whose order means nothing, such as a gallery of photos, and keep the default for anything read in sequence: steps, ranked results, articles.

Practise: place a 12-column page

Exercise · Easy · JavaScript

Work out where line-based placement puts each item

The lesson's page is a grid 1280px wide with grid-template-columns: repeat(12, 1fr) and gap: 24px. Its items are placed with values such as 1 / -1, 4 / span 3 and -4 / -1. Write three functions in placement.mjs and export them, so that they give the same numbers the three browsers measured. All lengths are in px.

The first, columnWidth(width, count, gap), returns the width of one column of repeat(count, 1fr) in a grid width px wide with gap px between the columns.

The second, resolveLines(value, count), returns the two lines an item sits between, as [start, end] with positive line numbers, in a grid of count explicit columns. value is a line number on its own ("3", one column wide), or two parts around a slash, where the first is a line number and the second a line number or span n. Line numbers may be negative: -1 is the last line of the explicit grid. Follow the grid's own rules for odd values: when the start line comes after the end line the two are swapped, and when they are the same line the item is one column wide.

The third, itemBox(value, width, count, gap), returns { x, width }: how far the item's left edge is from the grid's left edge, and how wide the item is, for the same kind of grid.

The sample tests import the three functions from placement.mjs and run in your browser. They accept answers within 0.01 px.

Starter code · placement.mjs

/** The width of one column of `repeat(count, 1fr)` in a grid `width` px wide with `gap` px gaps. */
export function columnWidth(width, count, gap) {
  // Replace this line with your code.
  return width / count;
}

/** The lines an item placed with `value` sits between: [start, end], positive line numbers. */
export function resolveLines(value, count) {
  // Replace this line with your code.
  return [1, 2];
}

/** Where an item placed with `value` is drawn: { x, width } in px from the grid's left edge. */
export function itemBox(value, width, count, gap) {
  // Replace this line with your code.
  return { x: 0, width };
}
The sample tests · placement.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { columnWidth, resolveLines, itemBox } from './placement.mjs';

const near = (actual, expected, what) => assert.ok(Math.abs(actual - expected) < 0.01, `${what}: expected ${expected}, got ${actual}`);
const box = (value, expected, width = 1280) => {
  const got = itemBox(value, width, 12, 24);
  near(got.x, expected.x, `x of "${value}"`);
  near(got.width, expected.width, `width of "${value}"`);
};

test('columns share what the gaps leave', () => {
  near(columnWidth(1280, 12, 24), 84.6667, 'repeat(12, 1fr) in 1280px with 24px gaps');
  near(columnWidth(1000, 4, 20), 235, 'repeat(4, 1fr) in 1000px with 20px gaps');
  near(columnWidth(600, 3, 0), 200, 'repeat(3, 1fr) in 600px without gaps');
});

test('positive lines, spans and the whole row', () => {
  assert.deepEqual(resolveLines('1 / -1', 12), [1, 13]);
  assert.deepEqual(resolveLines('4 / span 3', 12), [4, 7]);
  assert.deepEqual(resolveLines('3', 12), [3, 4]);
});

test('negative lines count from the end of the explicit grid', () => {
  assert.deepEqual(resolveLines('-4 / -1', 12), [10, 13]);
  assert.deepEqual(resolveLines('1 / -2', 4), [1, 4]);
});

test('lines in the wrong order are swapped, equal lines give one column', () => {
  assert.deepEqual(resolveLines('5 / 2', 12), [2, 5]);
  assert.deepEqual(resolveLines('6 / 6', 12), [6, 7]);
});

test('the lesson page at 1280px', () => {
  box('1 / -1', { x: 0, width: 1280 });
  box('1 / 4', { x: 0, width: 302 });
  box('4 / -1', { x: 326, width: 954 });
  box('4 / span 3', { x: 326, width: 302 });
  box('7 / span 3', { x: 652, width: 302 });
  box('-4 / -1', { x: 978, width: 302 });
});

test('the same page at 1000px', () => {
  box('1 / -1', { x: 0, width: 1000 }, 1000);
  box('2 / span 2', { x: 85.3333, width: 146.6667 }, 1000);
  box('-2', { x: 938.6667, width: 61.3333 }, 1000);
});
A hint

With count columns there are count - 1 gaps, so one column is (width - (count - 1) * gap) / count. A negative line n is line count + 2 + n, so -1 is line count + 1. An item from line a to line b starts after a - 1 columns and a - 1 gaps, and covers b - a columns and the b - a - 1 gaps between them.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Interview questions

Fresher · What does 1fr mean in grid-template-columns, and how is it different from a percentage?

1fr is one share of the grid’s leftover space: the space that remains after fixed and content-sized tracks and the gaps are taken away, divided among the fr tracks by their factors. A percentage is a share of the container’s whole width, worked out without the gaps. So in a 1000 px grid with two 20 px gaps, 1fr 2fr 1fr gives 240, 480 and 240 px and fits exactly, while 25% 50% 25% gives 250, 500 and 250 px and, with the gaps, overflows by 40 px. Two more differences matter. A bare 1fr never goes below its content’s minimum width, so a long word can widen it, which minmax(0, 1fr) prevents. And fr factors that add up to less than 1, such as a lone 0.5fr, take only that share of the space.

Key takeaways

  • display: grid turns the children into grid items; grid-template-columns and grid-template-rows define the explicit grid’s tracks.
  • Lines are numbered from 1 and from -1 at the end of the explicit grid; gaps sit on lines, never at the outer edges.
  • fr shares the space left after fixed tracks, content-sized tracks and gaps, so fr grids fit their container; percentage tracks plus gaps can overflow it.
  • auto tracks fit their content next to fr tracks, and stretch to share the space when there are none.
  • Place items with grid-column and grid-row: 1 / -1 for every column, 4 / span 3 for three columns, -4 / -1 for the last three.
  • Items beyond the explicit grid create implicit tracks sized by grid-auto-rows and grid-auto-columns; set them when the number of items can change.
  • dense fills holes but moves items away from the HTML order, which Tab and screen readers still follow; keep it for collections whose order means nothing.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 A grid 900px wide has grid-template-columns: 300px 1fr 1fr and column-gap: 30px. How wide is each 1fr column?

    Type a number.

    Show the answer to question 1

    Answer: 270 px

    The fr tracks share what the fixed track and the gaps leave: 900 − 300 − 2 × 30 = 540 px, divided by the two flex factors, so each 1fr column is 270 px wide. Gaps are subtracted first, which is why fr tracks never push a grid wider than its container.

  2. Question 2 of 6 A 1000px grid has grid-template-columns: 25% 50% 25% and gap: 20px. What happens?

    Choose one answer.

    Show the answer to question 2

    Answer: The columns are 250, 500 and 250 px, and with the two gaps they reach 1040 px, 40 px past the container.

    A percentage track is a share of the container's width, worked out before the gaps are added, so 100 % of tracks plus 40 px of gaps overflow. All three engines drew the columns 1040 px wide in the lesson's run. 1fr 2fr 1fr gives the same proportions and fits, because fr tracks share the space left after the gaps.

  3. Question 3 of 6 The lesson's page has 12 columns of 1fr in 1280px with 24px gaps. How wide is an item with grid-column: span 4?

    Type a number.

    Show the answer to question 3

    Answer: 410.67 px (anything from 410.66 to 410.68 counts)

    One column is (1280 − 11 × 24) ÷ 12 = 84.67 px. Four columns cover the three gaps between them too: 4 × 84.67 + 3 × 24 = 410.67 px. The gaps on the item's outer edges are not part of it.

  4. Question 4 of 6 A grid has grid-template-columns: repeat(3, 1fr). One item is placed with grid-column: 5, another with grid-column: 1 / -1. What does the second item cover?

    Choose one answer.

    Show the answer to question 4

    Answer: The three explicit columns, because -1 is the last line of the explicit grid

    Negative line numbers count back from the end of the explicit grid, the one grid-template-columns defines. The browser added columns 4 and 5 for the first item, but line -1 is still line 4, so in the lesson's run the second item was 460 px wide, the three explicit columns and their two gaps, out of a 600 px grid.

  5. Question 5 of 6 An item has grid-column: 6 / 2. Where does the browser put it?

    Choose one answer.

    Show the answer to question 5

    Answer: Between lines 2 and 6, because a start line after the end line is swapped

    Grid's placement rules swap the two lines when the start line comes after the end line, so 6 / 2 behaves like 2 / 6: four columns. When the two lines are the same line, the end line is dropped and the item is one column wide.

  6. Question 6 of 6 Which statement about grid-auto-flow: row dense is true?

    Choose one answer.

    Show the answer to question 6

    Answer: It fills earlier holes with later items, so the order on screen can differ from the order Tab and screen readers follow.

    Dense packing moves only the boxes on screen. In the lesson's run, tile D was drawn before tile C in all three engines, but Tab still went A, B, C, D, E, F. Use it for collections whose order carries no meaning, such as a gallery, and keep sparse placement for anything read in sequence.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.