Знакомая картина: три карточки в ряд, у одной заголовок в одну строку, у другой — в три. В итоге описания начинаются на разной высоте, а кнопки «Читать» стоят лесенкой. Сетка вокруг карточек ровная, а внутри каждой творится своё.
Такое выравнивание по-честному решается одним значением — subgrid. Разберём его с самого начала, медленно и с примером на каждый шаг. Из подготовки нужно только понимать, что такое display: grid и grid-template-columns.
Почему вложенная сетка не видит родителя
Типичная разметка карточки: заголовок, автор, описание, кнопка. Карточки лежат в сетке, сами карточки — флекс-колонки:
<div class="cards">
<article class="card">
<h3>Флексбокс</h3>
<p class="author">Анна</p>
<p class="text">Раскладка в одну линию.</p>
<a class="btn" href="#">Читать</a>
</article>
<!-- ещё две карточки -->
</div>
.cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.card {
display: flex;
flex-direction: column;
gap: 8px;
}
Сетка .cards отвечает только за то, где стоят сами карточки. Что внутри карточки, она не знает и знать не может: заголовок, автор и кнопка — это «внуки», а сетка раскладывает только прямых детей. Каждая карточка считает высоту своих частей сама, по своему содержимому, и никак не договаривается с соседками.
Если сделать карточку тоже гридом (display: grid), ничего не изменится: получится отдельная сетка со своими строками, которая про строки соседней карточки так же ничего не знает. Высота 100% или фиксированная высота заголовка тоже не помогают — первое растягивает карточку целиком, но не её части, второе ломается, как только текст станет длиннее.
Что делает subgrid
Одним предложением: элемент с subgrid не рисует свои линии сетки, а берёт линии родителя.
Удобно представить тетрадь в клетку. Обычная вложенная сетка — это вклеенный листок с собственной разлиновкой: клетки на нём другого размера и не совпадают с клетками тетради. Subgrid — это просто участок той же страницы, обведённый рамкой: клетки внутри рамки те же самые, что и снаружи.
Самый маленький пример. У родителя четыре колонки разной ширины и два ребёнка, занимающих колонки со 2-й по 4-ю:
.parent {
display: grid;
grid-template-columns: 80px 1fr 2fr 120px;
gap: 8px;
}
.child {
grid-column: 2 / 5; /* занимаем три колонки родителя */
display: grid;
}
Если первому написать grid-template-columns: repeat(3, 1fr), он поделит своё место на три равные части, и ни одна граница не совпадёт с родительской. Второму зададим grid-template-columns: subgrid и теперь его три колонки будут: 1fr, 2fr и 120px. Его дети встанут точно под колонками родителя.
Рецепт из трёх шагов
Чтобы subgrid заработал, нужны три вещи одновременно:
- Родитель — грид. Обычный display: grid, у него есть колонки и/или строки.
- Ребёнок занимает несколько треков родителя. Через grid-column или grid-row: 2 / 5, 1 / -1, span 4 — как удобнее. Трек — это одна колонка или одна строка сетки.
- Ребёнок тоже грид и говорит subgrid по нужной оси: grid-template-columns: subgrid для колонок, grid-template-rows: subgrid для строк.
.parent {
display: grid; /* шаг 1 */
}
.child {
grid-row: span 4; /* шаг 2: сколько строк родителя берём */
display: grid; /* шаг 3 */
grid-template-rows: subgrid; /* шаг 3 */
}
Главная ловушка — забыть про span
Если пропустить второй шаг, ребёнок по умолчанию займёт один трек родителя. Подсетка получит одну строку, и все дети попытаются в неё влезть. Новых строк subgrid по своей оси не создаёт, поэтому лишние элементы не переносятся вниз, а ложатся друг на друга:
/* сломано: заголовок, автор, текст и кнопка — в одной клетке */
.card {
display: grid;
grid-template-rows: subgrid;
}
/* починено: карточка занимает 4 строки — по одной на каждую часть */
.card {
grid-row: span 4;
display: grid;
grid-template-rows: subgrid;
}
Число в span должно совпадать с числом частей внутри. Добавили в карточку пятый элемент, например бейдж, — меняем на span 5, иначе бейдж наедет на кнопку.
Карточки с общими строками
Возвращаемся к карточкам из начала. Меняем флекс на подсетку по строкам:
.cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.card {
grid-row: span 4; /* заголовок, автор, текст, кнопка */
display: grid;
grid-template-rows: subgrid; /* строки — общие на весь ряд */
}
Важная деталь: у .cards нет grid-template-rows. Он и не нужен: карточка просит четыре строки, и родитель создаёт их автоматически. Высота каждой строки считается по самому высокому элементу в ней во всём ряду карточек. Самый длинный заголовок задаёт высоту строки заголовков, самое длинное описание — строки описаний, и кнопки встают на одну линию.
Во втором ряду карточек будет своя четвёрка строк со своими высотами, первый ряд на него не влияет.
Форма: подписи и поля в две ровные колонки
Вторая классика — форма, где подписи слева, поля справа, и все поля начинаются с одной вертикали. Каждую пару «подпись + поле» обычно оборачивают в div: туда же потом приедут подсказка, текст ошибки, фон при фокусе.
<form class="form">
<div class="field">
<label for="name">Имя</label>
<input id="name" type="text">
</div>
<div class="field">
<label for="email">Электронная почта</label>
<input id="email" type="email">
<p class="hint">Пришлём только подтверждение заказа</p>
</div>
</form>
Без subgrid у каждой обёртки своя ширина колонки подписи, и короткое «Имя» сдвигает своё поле левее, чем длинная «Электронная почта». С subgrid ширина колонки подписей одна на всю форму:
.form {
display: grid;
grid-template-columns: max-content 1fr; /* подписи — по самой длинной */
gap: 12px 16px;
}
.field {
grid-column: 1 / -1; /* обёртка на обе колонки */
display: grid;
grid-template-columns: subgrid;
}
.hint {
grid-column: 2; /* подсказка — строго под полем */
}
Значение max-content делает первую колонку ровно по ширине самой длинной подписи — подробнее про такие размеры в статье про min-content, max-content и fit-content. Подсказка с grid-column: 2 встаёт во вторую колонку на новой строке внутри обёртки — ровно под полем.
Похожий эффект можно получить, если убрать обёртки совсем или повесить на них display: contents. Но тогда у обёртки пропадает собственный блок: ей уже не задашь фон, рамку и отступы. Subgrid оставляет обёртку настоящим элементом.
Список «значок — текст — дата» как таблица без таблицы
Лента уведомлений, история заказов, список файлов: слева метка, в центре текст, справа дата. Хочется, чтобы метки были одной ширины, а даты стояли ровным столбиком — как в таблице. Но это не табличные данные, и <table> здесь был бы не к месту.
<ul class="feed">
<li>
<span class="badge">PR</span>
<span>Коллега одобрил изменения в ветке с формой</span>
<time>5 мин назад</time>
</li>
<li>
<span class="badge">Release</span>
<span>Вышла версия 2.4 с тёмной темой</span>
<time>2 недели назад</time>
</li>
</ul>
.feed {
display: grid;
grid-template-columns: auto 1fr auto; /* метка · текст · дата */
column-gap: 12px;
list-style: none;
}
.feed li {
grid-column: 1 / -1;
display: grid;
grid-template-columns: subgrid;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #e2e8f0;
}
Колонки с auto подстраиваются под самое широкое содержимое во всех строках сразу: самая длинная метка («Release») задаёт ширину первой колонки, самая длинная дата — третьей. При этом каждый <li> остаётся отдельным блоком с отступами и разделителем, и разметка остаётся семантичным списком.
Внутренний отступ padding у подсетки работает как обычно: крайние элементы просто отодвигаются от её краёв, на линии сетки это не влияет.
Ещё несколько возможностей
Subgrid сразу по двум осям
Ничто не мешает взять у родителя и колонки, и строки. Например, на дашборде из клеток 4×3 выделенный блок занимает квадрат 2×2, а его содержимое ложится ровно в те же клетки, что и соседние карточки:
.board {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(3, 80px);
gap: 12px;
}
.featured {
grid-column: 2 / 4;
grid-row: 1 / 3;
display: grid;
grid-template-columns: subgrid;
grid-template-rows: subgrid;
}
У .featured внутри четыре клетки, и они совпадают с клетками всей доски — вместе с промежутками между ними.
Subgrid внутри subgrid
Подсетку можно вложить в подсетку — линии передаются вниз по цепочке. Пример: меню кофейни, где есть группы, а в группах строки. Колонки «название / объём / цена» должны совпадать во всех группах:
.menu {
display: grid;
grid-template-columns: 1fr auto auto;
gap: 8px 24px;
}
/* уровень 1: группа берёт колонки меню */
.group {
grid-column: 1 / -1;
display: grid;
grid-template-columns: subgrid;
}
.group h3 {
grid-column: 1 / -1;
}
/* уровень 2: строка берёт колонки группы — а значит, и меню */
.row {
grid-column: 1 / -1;
display: grid;
grid-template-columns: subgrid;
}
Даже если самое длинное значение объёма («1 000 мл») живёт в группе «Чай», колонка объёма в группе «Кофе» будет той же ширины. Обе группы смотрят на одну и ту же сетку .menu.
Свой gap внутри подсетки
Промежутки между треками подсетка по умолчанию берёт у родителя. В карточках это часто мешает: между рядами карточек нужно 32px воздуха, а между заголовком и автором внутри карточки — 6px. Решение — задать подсетке свой row-gap:
.cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 32px 16px; /* 32px между рядами карточек */
}
.card {
grid-row: span 4;
display: grid;
grid-template-rows: subgrid;
row-gap: 6px; /* внутри карточки плотнее */
}
Строки при этом остаются общими: свой gap меняет только расстояние между частями внутри подсетки, но не её выравнивание с соседями.
Имена линий доходят до подсетки
Если у линий родителя есть имена, внутри подсетки ими можно пользоваться напрямую. Самый практичный случай — раскладка статьи: текст в узкой колонке посередине, а картинки или цитаты при желании на всю ширину экрана.
.page {
display: grid;
grid-template-columns:
[full-start] minmax(16px, 1fr)
[content-start] minmax(0, 520px)
[content-end] minmax(16px, 1fr)
[full-end];
}
.article {
grid-column: full; /* статья на всю ширину... */
display: grid;
grid-template-columns: subgrid; /* ...но с линиями родителя */
}
.article > * {
grid-column: content; /* имя пришло от .page */
}
.article > .wide {
grid-column: full;
}
Пара линий content-start / content-end автоматически даёт область с именем content, поэтому достаточно написать grid-column: content. Без subgrid внутри .article этих имён бы не было, и колонки пришлось бы описывать заново. Подсетка может добавить и собственные имена линий, например grid-template-columns: subgrid [a] [b], — они просто дополнят родительские.
Частые ошибки
- Нет span или он меньше числа частей. Подсетка получает слишком мало треков, и лишние элементы наползают друг на друга в последнем. Считаем детей и ставим столько же.
- Забыли display: grid у ребёнка. Значение subgrid работает только у элемента, который сам является грид-контейнером.
- Ребёнок не прямой потомок сетки. Subgrid передаёт линии только на один уровень вниз. Если между .cards и .card затесалась обёртка, например ссылка вокруг карточки, — подсеткой должна стать и обёртка, или нужно убрать её из цепочки.
- Ждут выравнивания между рядами. Карточки первого ряда выравниваются друг с другом, второго — друг с другом, но строки первого ряда не влияют на второй. Это поведение по задумке, а не баг.
Если браузер не знает subgrid
Для браузеров, которые понимают только обычный грид, удобно сделать базовую раскладку на флексе, а подсетку включать через @supports (подробно это правило разобрано в статье про at-правила CSS):
/* база: флекс, описание растягивается и прижимает кнопку к низу */
.card {
display: flex;
flex-direction: column;
gap: 8px;
}
.card .text {
flex: 1;
}
/* улучшение: если браузер знает subgrid */
@supports (grid-template-rows: subgrid) {
.card {
display: grid;
grid-row: span 4;
grid-template-rows: subgrid;
}
}
Карточки в ряду и так одной высоты (грид их растягивает), а flex: 1 у описания съедает свободное место и уводит кнопку вниз. Заголовки в старом браузере не выровняются, но кнопки встанут в линию — для фолбэка этого достаточно. В браузере с поддержкой сработает блок внутри @supports, а flex: 1 в гриде просто ни на что не влияет.
Коротко
- Subgrid нужен, когда элементы внутри разных блоков должны стоять на общих линиях: части карточек, поля формы, столбцы списка.
- Три шага: родитель — грид; ребёнок занимает нужное число треков; ребёнок — грид с grid-template-columns: subgrid и/или grid-template-rows: subgrid.
- Без span подсетка получает один трек — самая частая причина, почему «ничего не работает».
- Промежутки и имена линий приходят от родителя; gap можно переопределить, имена — дополнить.
- Подсетки вкладываются друг в друга, линии передаются по цепочке.
Полный справочник по значению с деталями по каждому случаю — на MDN.
Комментарии (0)