Задача
СкопированоПлиточная раскладка — это метод компоновки, при котором блоки по одной из осей вписываются в стандартную сетку (занимают определённое количество колонок или строк), а по другой оси располагаются так, чтобы не оставалось лишнего пространства, как бы прижимаются вплотную друг к другу.
В 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; }
Количество скрытых колонок-разделителей равно количеству колонок минус один. В разметке эти элементы должны располагаться после всех видимых элементов, в самом конце контейнера. Их фактическое расположение определяется стилями.