Задача
СкопированоПлиточная раскладка — это метод компоновки, при котором блоки по одной из осей вписываются в стандартную сетку (занимают определённое количество колонок или строк), а по другой оси располагаются так, чтобы не оставалось лишнего пространства, как бы прижимаются вплотную друг к другу.
В 2020 году был опубликован черновик спецификации CSS Grid Level 3. В этом документе описывается простой способ создания плиточной раскладки. Для реализации необходимо создать контейнер с блоками и задать одной из осей контейнера свойство grid со значением masonry. Выглядит это следующим образом:
.container { display: grid; grid-template-rows: masonry; grid-template-columns: repeat(3, 1fr);}
.container {
display: grid;
grid-template-rows: masonry;
grid-template-columns: repeat(3, 1fr);
}
В данный момент эта технология экспериментальная и пока доступна только в браузере Firefox Nightly. Nightly — это нестабильная версия для активных пользователей, которые хотят участвовать в тестировании новых функций.
Существует решение этой задачи на чистом CSS, но у него есть свои нюансы. Давайте разбираться.
Готовое решение
СкопированоРазметка — контейнер с блоками разной высоты:
<div class="container"> <div class="item" style="height: 120px"><span>1</span></div> <div class="item" style="height: 200px"><span>2</span></div> <div class="item" style="height: 150px"><span>3</span></div> <div class="item" style="height: 100px"><span>4</span></div> <div class="item" style="height: 110px"><span>5</span></div> <div class="item" style="height: 120px"><span>6</span></div> <div class="item" style="height: 100px"><span>7</span></div> <div class="item" style="height: 200px"><span>8</span></div> <div class="item" style="height: 110px"><span>9</span></div></div>
<div class="container">
<div class="item" style="height: 120px"><span>1</span></div>
<div class="item" style="height: 200px"><span>2</span></div>
<div class="item" style="height: 150px"><span>3</span></div>
<div class="item" style="height: 100px"><span>4</span></div>
<div class="item" style="height: 110px"><span>5</span></div>
<div class="item" style="height: 120px"><span>6</span></div>
<div class="item" style="height: 100px"><span>7</span></div>
<div class="item" style="height: 200px"><span>8</span></div>
<div class="item" style="height: 110px"><span>9</span></div>
</div>
Стили:
.container { display: flex; flex-wrap: wrap; flex-direction: column; gap: 8px 4px; /* высота контейнера фиксированная */ /* должна быть больше любой из колонок */ height: 600px; width: 100%;}.item { width: calc(100% / 3);}.item:nth-child(3n + 1) { order: 1; }.item:nth-child(3n + 2) { order: 2; }.item:nth-child(3n) { order: 3; }.container::before,.container::after { content: ""; flex-basis: 100%; width: 0; order: 2;}
.container {
display: flex;
flex-wrap: wrap;
flex-direction: column;
gap: 8px 4px;
/* высота контейнера фиксированная */
/* должна быть больше любой из колонок */
height: 600px;
width: 100%;
}
.item {
width: calc(100% / 3);
}
.item:nth-child(3n + 1) { order: 1; }
.item:nth-child(3n + 2) { order: 2; }
.item:nth-child(3n) { order: 3; }
.container::before,
.container::after {
content: "";
flex-basis: 100%;
width: 0;
order: 2;
}
Разбор решения
СкопированоСоздаём флекс-контейнер, внутрь кладём блоки разной высоты. Задаём свойство flex со значением wrap — нам нужно, чтобы блоки располагались в несколько рядов. Теперь определим направление расположения блоков внутри контейнера. На этом моменте и появляются первые сложности.
Если установить на контейнере свойство flex со значением row, то блоки будут размещаться в строку. При этом высота строки будет равна высоте наибольшего блока. Таким образом, по вертикальной оси между соседними блоками будет оставаться пустое пространство.
Если установить flex со значением column, то нужно также указать фиксированную высоту контейнера (иначе блоки не будут переноситься в новую колонку). Для динамической подгрузки данных, когда количество блоков и необходимая высота контейнера неизвестны, такое решение не подойдёт. В этом случае стоит смотреть в сторону решения на JavaScript.
Ещё один недостаток — нарушается удобный при чтении слева направо порядок элементов. Но это можно победить, переопределив порядок элементов.
Меняем порядок блоков
СкопированоПереопределим порядок блоков с помощью свойства order. Блок с меньшим числовым значением встаёт перед блоком с бо̀льшим значением, вне зависимости от фактического расположения блоков в HTML-разметке.
Сейчас, при чтении слева направо, пользователь видит блоки в следующем порядке:
Первый элемент и каждый третий после него (3n+1) должны рендериться первыми. Устанавливаем для них order. Затем должны идти элементы 3n+2, и в последнюю очередь — элементы 3n. Для выбора элементов по формуле воспользуемся псевдоклассом :nth.
.item:nth-child(3n+1) { order: 1; }.item:nth-child(3n+2) { order: 2; }.item:nth-child(3n) { order: 3; }
.item:nth-child(3n+1) { order: 1; }
.item:nth-child(3n+2) { order: 2; }
.item:nth-child(3n) { order: 3; }
Добавляем колонки-разделители
СкопированоКазалось бы, порядок элементов исправлен. Но такой подход сработает не всегда. Предположим, у нас есть колонка с невысокими блоками. Они вписываются по высоте в контейнер, и ещё остаётся значительное пространство под ними. В таком случае блок, который должен рендериться в следующей колонке, может не перенестись и попасть не в свою колонку.
В примере ниже белым цветом обозначены блоки, которые не перенеслись в новую колонку, а вписались по размеру в предыдущую. Элемент 2 отрендерился в первой колонке, хотя должен быть во второй. Элемент 3 — во второй вместо третьей.
Нам нужно как-то контролировать переносы элементов в новые колонки. Для решения этой задачи создадим дополнительные скрытые колонки-разделители. Разместим эти скрытые колонки между основными. Установим высоту в 100% — так, чтобы другие элементы не сливались со скрытой колонкой в одну.
/* Создаём две скрытые колонки с помощью псевдоэлементов */.container::before,.container::after { content: ""; flex-basis: 100%; width: 0; order: 2;}
/* Создаём две скрытые колонки с помощью псевдоэлементов */
.container::before,
.container::after {
content: "";
flex-basis: 100%;
width: 0;
order: 2;
}
Готово!
А что, если колонок больше трёх?
СкопированоЕсли колонок больше, нам нужно адаптировать решение, а именно:
- поправить ширину колонок;
- добавить дополнительные колонки-разделители и поправить их расположение.
В случае с тремя колонками мы создавали две дополнительные скрытые колонки с помощью псевдоэлементов : и :. При большем числе видимых колонок число скрытых также растёт, и псевдоэлементы уже не подходят. Используем обычный <div>. К элементу добавляем два класса — item и break. Класс item используется как для видимых элементов, так и для скрытых. Класс break — только для скрытых.
Разметка для скрытой колонки:
<div class="item break"></div>
<div class="item break"></div>
Стили:
.break { flex-basis: 100%; width: 0; margin: 0;}
.break {
flex-basis: 100%;
width: 0;
margin: 0;
}
Количество скрытых колонок-разделителей равно количеству колонок минус один. В разметке эти элементы должны располагаться после всех видимых элементов, в самом конце контейнера. Их фактическое расположение определяется стилями.