Перейти к содержимому

CSS Grid в деталях: fr, minmax, areas, паттерны

CSS Grid — первая в истории CSS система раскладки, спроектированная как двумерная с первого принципа. Flexbox ведёт элементы вдоль одной оси и переносит; Grid сразу оперирует колонками И строками, как таблица, но без её ограничений. Здесь ты собираешь макеты страниц, карточные сетки и формы — там, где flex-вложенность превращалась в лапшу.

В продакшене grid — инструмент макетного уровня: оболочка страницы, сетка карточек, разметка формы. Его киллер-фича grid-template-areas делает макет читаемым как ASCII-картинку, а auto-fill/minmax() позволяют строить отзывчивые сетки вообще без медиазапросов. Но у grid своя терминология и свои грабли: fr не то, чем кажется, auto-fill молча создаёт пустые треки, а 1fr с minmax() ведёт себя неожиданно при переполнении.

Терминология: контейнер, линии, треки, ячейки, области

Заголовок раздела «Терминология: контейнер, линии, треки, ячейки, области»
grid-контейнер:
линии (lines) — вертикальные 1..N, горизонтальные 1..M
треки (tracks) — пространство МЕЖДУ двумя соседними линиями
ячейки (cells) — пересечение трека строки и трека колонки
области (areas) — прямоугольник из одной или нескольких ячеек
линии колонок
1 2 3 4
1 ┌────────┬────────┬────────┐
│ │ │ │
│ area A │ area A │ area B │ ← трек строки 1
│ │ │ │
2 ├────────┼────────┤ │
│ │ │ │
│ area C │ area D │ area B │ ← трек строки 2
│ │ │ │
3 └────────┴────────┴────────┘
▲ ▲ ▲
└────────┴────────┴─ треки колонок (3 штуки)

Айтем размещается по линиям: grid-column: 1 / 3 — от линии 1 до линии 3 (два трека). Все счёты — по линиям, не по ячейкам: в сетке из 3 колонок линий 4.

.page {
display: grid;
grid-template-columns: 260px 1fr 300px; /* 3 колонки */
grid-template-rows: auto 1fr auto; /* 3 строки: header, main, footer */
min-height: 100dvh;
}

fr (fraction) — доля свободного места ПОСЛЕ вычитания жёстких треков и gap’ов:

контейнер 1000px, gap 20px × 2 = 40px, колонки: 260px 1fr 300px
свободное = 1000 − 40 − 260 − 300 = 400px
1fr = 400px (вся свободная доля на один трек)
итог: 260 | 20 | 400 | 20 | 300
если бы было: 1fr 2fr 1fr → свободных 400 делятся на 4 доли → 100 / 200 / 100

Ключевой подвох: fr — это minmax(auto, Nfr), то есть трек не может быть уже содержимого! Длинный неразрывный текст в 1fr-колонке раздует её и сломает сетку. Лечится тем же приёмом, что и во flex: min-width: 0 на айтеме, либо явным minmax(0, 1fr). Формально это записано в спецификации CSS Grid Layout Module Level 2 — раздел про fr-единицы и track sizing algorithm.

/* «настоящий» гибкий трек: старт с нуля, не реагирует на контент */
grid-template-columns: minmax(0, 1fr);

repeat(n, значение) — сокращение: repeat(3, 1fr) = 1fr 1fr 1fr. Внутри можно комбинировать: repeat(2, 120px 1fr).

minmax(min, max) — трек живёт между границами. Звезда адаптивных сеток:

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

Контейнер сам решает, сколько колонок поместится: при 1200px — 5, при 700px — 3, при 360px — 1. Медиазапросы не нужны. Теперь разница двух близнецов, которая выносит мозг:

auto-fill: пустые треки СОХРАНЯЮТСЯ auto-fit: пустые треки СХЛОПЫВАЮТСЯ
контейнер 1000px, minmax(240px, 1fr), 2 карточки:
┌────┐┌────┐┌────┐┌────┐ (пустые!) ┌──────────┐┌──────────┐
│card││card││ ││ │ │ card ││ card │
└────┘└────┘└────┘└────┘ └──────────┘└──────────┘
4 колонки: карточки узкие, 2 колонки: карточки
справа пустое место растянулись на всю ширину
fill — количество колонок фиксировано fit — количество = числу айтемов
(полезно с явным justify-контролем)

Правило большого пальца: в 95% случаев для карточных сеток нужен auto-fit. auto-fill оправдан, когда ты сознательно держишь сетку «в натяг» (например, галерея с фиксированным числом колонок для определённого breakpoint’а). Разница близнецов с иллюстрациями — в MDN: Автоматическое размещение в Grid.

grid-template-columns: fit-content(300px) 1fr; /* трек = по контенту, но не шире 300 */
grid-template-columns: min(400px, 60%) 1fr; /* жёсткий трек: минимум из двух */
grid-auto-rows: minmax(120px, auto); /* все НЕявные строки такие */

auto-rows/auto-columns определяют размер треков, созданных неявно (айтемов больше, чем явных строк).

Размещение айтемов: по линиям и по областям

Заголовок раздела «Размещение айтемов: по линиям и по областям»
.header { grid-column: 1 / -1; } /* от первой линии до ПОСЛЕДНЕЙ (-1) */
.sidebar { grid-row: 2 / 4; } /* строки 2..4 */

Шорткат grid-area: row-start / col-start / row-end / col-end. Значение span 2 — «захвати два трека».

.page {
display: grid;
grid-template-columns: 260px 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
min-height: 100dvh;
}
.header { grid-area: header }
.sidebar { grid-area: sidebar }
.main { grid-area: main }
.footer { grid-area: footer }
Визуально то же, что и areas-запись:
┌─────────────────────────┬─────────────────────────┐
│ header │ header │
├─────────────────────────┼─────────────────────────┤
│ sidebar │ main │
├─────────────────────────┼─────────────────────────┤
│ footer │ footer │
└─────────────────────────┘└─────────────────────────┘
одно и то же слово в соседних ячейках = одна область.
точка "." = пустая ячейка: "sidebar ." ← main занимает колонку 2-3

Перестройка макета под другой breakpoint — переписать одну строку-матрицу:

@media (max-width: 800px) {
.page {
grid-template-columns: 1fr;
grid-template-areas:
"header"
"main"
"sidebar"
"footer";
}
}

Медиазапросы подробно — в главе про адаптив; здесь важно: areas — самый читаемый способ описать «holy grail» макет. Визуальная шпаргалка по всем grid-свойствам (включая areas) — CSS Tricks: A Complete Guide to Grid.

Как и во flex, два направления. justify-* — по оси колонок, align-* — по оси строк:

Свойство Действие Аналог во flex
justify-items выравнивание айтемов В ЯЧЕЙКАХ по горизонтали align-items в row
align-items выравнивание в ячейках по вертикали то же, семантика совпадает
justify-content распределение всей сетки, если она уже контейнера то же
align-content то же по вертикали для строк align-content
justify-self / align-self переопределение для одного айтема align-self

По умолчанию айтемы растягиваются (stretch) на всю ячейку — поэтому grid-карточки естественно одинаковой высоты в строке, без вложенных флексов.

grid-auto-flow: dense — при явном размещении «дырки» заполняются следующими подходящими айтемами (плотная упаковка, как в masonry). Внимание: порядок визуальный съезжает — те же риски доступности, что и order во flex.

subgrid — айтем-грид наследует линии РОДИТЕЛЬСКОЙ сетки:

.cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.card {
display: grid;
grid-template-rows: subgrid; /* строки карточки = строки .cards */
grid-row: span 3; /* каждая карточка занимает 3 строки сетки */
}

Эффект: заголовки всех карточек стоят на одной горизонтали, кнопки — на другой, хотя высота контента у всех разная. Раньше для этого городили вложенные гриды с фиксированными размерами. Поддержка: Firefox с 71, Chrome/Edge с 117, Safari 16+ — уже можно.

Masonry (кирпичная кладка Pinterest) — пока только в Firefox за флагом и спецификацией в дискуссии (grid-template-rows: masonry / grid-template-columns: masonry). В продакшене — JS-решения или column-count. Подробности — в разделе про современный CSS.

Header и footer на всю ширину, сайдбар слева, main справа, равная высота колонок по контенту:

<body class="page">
<header class="header">Шапка</header>
<aside class="sidebar">Сайдбар</aside>
<main class="main">Контент</main>
<footer class="footer">Футер</footer>
</body>
.page {
display: grid;
grid-template-columns: 260px minmax(0, 1fr);
grid-template-rows: auto minmax(0, 1fr) auto;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
min-height: 100dvh;
}

На flex это была связка из двух вложенных флексов + хаки для равной высоты. На grid — одна декларация, колонки равной высоты бесплатно.

.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
gap: 1.5rem;
}

min(100%, 240px) внутри minmax закрывает граблю мобильных: без него при ширине вьюпорта < 240px+padding’и сетка переполнялась бы. Эта одна строка заменяет четыре медиазапроса «2 колонки → 3 → 4 → 6».

.form {
display: grid;
grid-template-columns: max-content minmax(0, 1fr); /* label по контенту, input — остаток */
gap: 0.75rem 1rem;
align-items: center;
}
<form class="form">
<label for="email">Email</label>
<input id="email" type="email">
<label for="pass">Пароль</label>
<input id="pass" type="password">
</form>

Каждая пара label+input сама садится в свою строку сетки (автоплейсмент по строкам), выравнивание по центру по вертикали — align-items: center одним свойством. Раньше — float’ы и clearfix’ы.

  1. auto-fill вместо auto-fit в карточной сетке — самая частая: карточки не растягиваются, справа пустая колонка. Вспомни схему: fill хранит пустые треки.
  2. 1fr ведёт себя как min-content при переполнении — длинный контент ломает сетку. Лечится minmax(0, 1fr) или min-width: 0 на айтеме.
  3. Путаница линий/ячеек при размещении — «почему grid-column: 1 / 2 заняло ДВЕ колонки?» Нет: линия 1 до линии 2 — это ОДИН трек. Счёт всегда по линиям.
  4. Пустые области без точек — в grid-template-areas каждая строка матрицы должна иметь одинаковое число ячеек; «дырку» помечают .: "header .", а не пропуском.
  5. Ожидание, что gap участвует в fr-расчёте — gap’ы вычитаются из свободного места до деления на доли (см. схему расчёта). Это правильно, но объясняет, почему «не сходится» при ручном подсчёте без gap’ов.
  6. Masonry через grid — «почти работает» — только Firefox/за флагом. Проверяй caniuse перед использованием, в остальных браузерах будет обычная построчная раскладка.
  7. Нумерация с -1 при НЕявных трекахgrid-column: 1 / -1 тянет айтем до конца ЯВНОЙ сетки; неявные треки за -1 не считаются.
  1. Чем grid принципиально отличается от flexbox? Grid двумерный по замыслу: одновременно колонки и строки, размещение по линиям/областям. Flex одномерен: одна ось, вторая — вспомогательное выравнивание. Макет страницы и карточные сетки — grid; компонентные ряды — flex.
  2. Что такое fr и как он считается? Доля свободного места после вычитания жёстких треков и gap’ов: итог = свободное × (fr / Σfr). По сути это minmax(auto, Nfr) — нижняя граница по контенту, поэтому для «чистого» деления нужен minmax(0, 1fr).
  3. Разница auto-fill и auto-fit? Оба создают столько треков, сколько влезает. Fill оставляет пустые треки (сетка «держит» количество колонок), fit схлопывает пустые в ноль (колонки перераспределяются между реальными айтемами). Для карточек почти всегда fit.
  4. Как работает grid-template-areas? Декларативная матрица: строки в кавычках, ячейки — имена областей, повтор имени в соседних ячейках склеивает их в одну область, точка — пустая ячейка. Айтем получает область через grid-area: имя. Позволяет перестраивать макет одной медиазапросной правкой.
  5. Что такое subgrid и какую задачу решает? Вложенный грид наследует линии родителя (grid-template-rows: subgrid), поэтому элементы внутри разных карточек выравниваются по общей сетке (заголовки на одной линии при разной высоте контента). Поддержка: все современные браузеры с конца 2023.
  6. Как сделать адаптивную карточную сетку без медиазапросов? repeat(auto-fit, minmax(min(100%, 240px), 1fr)) с gap. Сетка сама добавляет/убирает колонки по ширине контейнера; min(100%, …) защищает от переполнения на узких экранах.
  7. Что произойдёт, если айтемов больше, чем явных ячеек сетки? Создаются неявные треки с размером из grid-auto-rows/grid-auto-columns (по умолчанию auto). Позиционирование новых айтемов идёт по grid-auto-flow (row по умолчанию).
  8. Почему min-width: 0 нужен и в flex, и в grid? И там, и там дефолтный минимальный размер трека/айтема равен min-content (через min-width: auto у flex-айтема и auto в fr), поэтому неразрывный контент раздувает раскладку. Ноль снимает защиту.
  1. Holy Grail. Собери макет header/sidebar/main/footer через grid-template-areas, затем через одну медиаправку переключи на одноколоночный порядок «header, main, sidebar, footer». Критерий: перестройка — только изменение одной строки-матрицы, без правок классов айтемов.
  2. Сетка без медиазапросов. Карточная сетка на auto-fit + minmax(min(100%, 240px), 1fr). Проверь на 320px, 768px, 1440px: ни один breakpoint не написан руками. Критерий: карточки не переполняются при узком вьюпорте.
  3. Карточки в одну линию через subgrid. Три карточки с разным текстом: заголовки на одной горизонтали, кнопки — на другой. Реши дважды: через subgrid и через flex с margin-top: auto. Критерий: оба решения работают, можешь назвать плюсы каждого.
  4. Расчёт fr. Контейнер 1200px, gap 24px × 3, колонки 200px 1fr 2fr 1fr. Посчитай ширины на бумаге по формуле, сверь с DevTools. Критерий: расхождение не более 1px.
  5. Автоплейсмент. Сетка 4 колонки, 8 айтемов без явного размещения. Добавь grid-auto-flow: dense и размести 3-й айтем через grid-column: span 2. Наблюдай за скачком порядка. Критерий: объясни, почему 5-й айтем «переехал» и чем это опасно для доступности.

Компаньон этой главы — игра Grid Garden: 28 уровней от grid-column-start до repeat() и minmax().