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.
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.
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.
- 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.
- The row lines are numbered 1, 2 and 3 from the top on the left, and -3, -2 and -1 on the right.
- 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.
- The cell in row 1, column 1 is highlighted: one row track crossing one column track.
- 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 withgap) 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:
<!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:
frshares out the leftover space. The browser takes the fixed tracks and the gaps away first: 1000 − 200 − 2 × 20 leaves 760 px, and1fr 2frdivides 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.autofits the content when fr tracks are present. “Menu” in 16 px Arial is 40.03 px wide, so theautocolumn took exactly that and1frthe rest.autotracks stretch when nothing else claims the space. With twoautocolumns 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.5frcolumn 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 1frgives 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:
// 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
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
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.
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:
<!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.
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:
<!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 explicit1frcolumns shrank to 146.67 px to make room: 600 − 2 × 60 − 4 × 10 = 440 px, split three ways. grid-column: 1 / -1beside 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:
<!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:
// 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
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
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.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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: gridturns the children into grid items;grid-template-columnsandgrid-template-rowsdefine 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.
frshares the space left after fixed tracks, content-sized tracks and gaps, so fr grids fit their container; percentage tracks plus gaps can overflow it.autotracks fit their content next to fr tracks, and stretch to share the space when there are none.- Place items with
grid-columnandgrid-row:1 / -1for every column,4 / span 3for three columns,-4 / -1for the last three. - Items beyond the explicit grid create implicit tracks sized by
grid-auto-rowsandgrid-auto-columns; set them when the number of items can change. densefills 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.
References
- CSS Grid Layout Module Level 2: grid layout concepts and terminology (W3C CSS Working Group)
- CSS Grid Layout Module Level 2: flexible lengths, the fr unit (W3C CSS Working Group)
- CSS Grid Layout Module Level 2: find the size of an fr (W3C CSS Working Group)
- CSS Grid Layout Module Level 2: stretch auto tracks (W3C CSS Working Group)
- CSS Grid Layout Module Level 2: line-based placement (W3C CSS Working Group)
- CSS Grid Layout Module Level 2: grid placement conflict handling (W3C CSS Working Group)
- CSS Grid Layout Module Level 2: the implicit grid (W3C CSS Working Group)
- CSS Grid Layout Module Level 2: resolved value of a track listing (W3C CSS Working Group)
- CSS Grid Layout Module Level 2: grid item placement algorithm (W3C CSS Working Group)
- CSS Grid Layout Module Level 2: reordering and accessibility (W3C CSS Working Group)
- CSS Grid Layout Module Level 1 (W3C CSS Working Group)
- CSS Snapshot 2026: reliable Candidate Recommendations (W3C CSS Working Group)
- Web features explorer: Grid (W3C WebDX Community Group)
- Web features explorer: reading-flow (W3C WebDX Community Group)
- reading-flow (MDN Web Docs) (Mozilla)
- CSS grid layout (MDN Web Docs) (Mozilla)
- Understanding Success Criterion 1.3.2: Meaningful Sequence (W3C Web Accessibility Initiative)
- Understanding Success Criterion 2.4.3: Focus Order (W3C Web Accessibility Initiative)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress