Плиточная раскладка

Как реализовать плиточную раскладку на чистом CSS во времена со слабой поддержкой grid-template-rows: masonry.

Время чтения: 6 мин

Задача

Скопировано

Плиточная раскладка — это метод компоновки, при котором блоки по одной из осей вписываются в стандартную сетку (занимают определённое количество колонок или строк), а по другой оси располагаются так, чтобы не оставалось лишнего пространства, как бы прижимаются вплотную друг к другу.

В 2020 году был опубликован черновик спецификации CSS Grid Level 3. В этом документе описывается простой способ создания плиточной раскладки. Для реализации необходимо создать контейнер с блоками и задать одной из осей контейнера свойство grid-template-* со значением 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 со значением wrap — нам нужно, чтобы блоки располагались в несколько рядов. Теперь определим направление расположения блоков внутри контейнера. На этом моменте и появляются первые сложности.

Если установить на контейнере свойство flex-direction со значением row, то блоки будут размещаться в строку. При этом высота строки будет равна высоте наибольшего блока. Таким образом, по вертикальной оси между соседними блоками будет оставаться пустое пространство.

Открыть демо в новой вкладке

Если установить flex-direction со значением column, то нужно также указать фиксированную высоту контейнера (иначе блоки не будут переноситься в новую колонку). Для динамической подгрузки данных, когда количество блоков и необходимая высота контейнера неизвестны, такое решение не подойдёт. В этом случае стоит смотреть в сторону решения на JavaScript.

Ещё один недостаток — нарушается удобный при чтении слева направо порядок элементов. Но это можно победить, переопределив порядок элементов.

Открыть демо в новой вкладке

Меняем порядок блоков

Скопировано

Переопределим порядок блоков с помощью свойства order. Блок с меньшим числовым значением встаёт перед блоком с бо̀льшим значением, вне зависимости от фактического расположения блоков в HTML-разметке.

Сейчас, при чтении слева направо, пользователь видит блоки в следующем порядке:

Открыть демо в новой вкладке

Первый элемент и каждый третий после него (3n+1) должны рендериться первыми. Устанавливаем для них order: 1. Затем должны идти элементы 3n+2, и в последнюю очередь — элементы 3n. Для выбора элементов по формуле воспользуемся псевдоклассом :nth-child.

        
          
          .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;
}

        
        
          
        
      
Открыть демо в новой вкладке

Готово!

А что, если колонок больше трёх?

Скопировано

Если колонок больше, нам нужно адаптировать решение, а именно:

  • поправить ширину колонок;
  • добавить дополнительные колонки-разделители и поправить их расположение.

В случае с тремя колонками мы создавали две дополнительные скрытые колонки с помощью псевдоэлементов ::before и ::after. При большем числе видимых колонок число скрытых также растёт, и псевдоэлементы уже не подходят. Используем обычный <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;
}

        
        
          
        
      

Количество скрытых колонок-разделителей равно количеству колонок минус один. В разметке эти элементы должны располагаться после всех видимых элементов, в самом конце контейнера. Их фактическое расположение определяется стилями.

Открыть демо в новой вкладке