Плиточная раскладка на CSS Flexbox с помощью order и :nth-child()

Stas Bagretsov
9 min readMay 16, 2019

--

Эта статья показывает то, как сделать плиточную раскладку на CSS Flexbox, имея под рукой только стандартные возможности CSS и ничего более. Она хороша тем, что показывает подводные камни работы с флексами и даёт понять суть их работы с другого ракурса.

Перевод статьи CSS masonry with flexbox, :nth-child(), and order

👉Мой Твиттер — там много из мира фронтенда, да и вообще поговорим🖖. Подписывайтесь, будет интересно: ) ✈️

С первого взгляда это кажется довольно простым, взять и создать плиточную раскладку на флексах. Всё, что нужно, это всего лишь выставить flex-flow на column wrap и вуаля, у вас то что нужно. Ну типа того. Проблема с таким подходом в том, что он создает сетку с на вид перетасованным и неясным порядком. Элементы будут отрендерены сверху вниз и люди, смотрящие на сетку слева направо будут читать боксы в примерно таком, довольно произвольном виде, к примеру 1, 3, 6, 2, 4, 7, 8, 5 и так далее и т.п.

У флексов нет простого способа отрендерить элементы в виде колоночной раскладки, используя строчную расстановку элементов, но мы всё же можем сделать плиточную раскладку только на CSS — без JavaScript — используя свойства :nth-child() и order. По сути, это трюк с созданием строчной расстановки элементов, используя flex-direction: column, учитывая то, что мы рендерим 3 колонки:

/*  Рендерим элементы как колонки */
.container {
display: flex;
flex-flow: column wrap;
}

/* Меняем порядок элементов, делая как бы строчную расстановку */
.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;
}

Это создаст плиточную раскладку с элементами, отрендереными как колонки, но расставленными в порядке, как было бы в строчном порядке. Серые вертикальные линии это псевдоэлементы, которые делают переносы строк.

Давайте разберем проблему (ну или перейдите к её решению в конце статьи или к codepen).

Выбирайте каким будет косяк: смешанным порядком или странными промежутками

Flexbox по факту не создан для построения плиточных раскладок — если вы выставите фиксированную высотку на флекс-контейнере и flex-flow на column wrap, то у вас получится что-то типа такого:

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

Если же мы сменим flex-direction на row, имея элементы с различными высотами, то мы получим правильный порядок, но уже со стрёмными и неожиданными разрывами по все нашей сетке:

Так, что, кажется невозможным воспользоваться преимуществами обоих подходов: получить элементы отрендеренные как колонки, но имеющие строчный порядок. Вы могли прибегнуть к flex-direction: column и просто переставить элементы в HTML, достигнув правильного визуального порядка, но это может быть очень громоздко, к тому же это излишне сложно и создаст неразбериху в логическом порядке при переходах по клавише табуляции (Просто кликая по табу).

Меняем порядок элементов с помощью order и nth-child()

Свойство order влияет на порядок элементов в CSS Flexbox или гриде, и мы можем смело использовать его для смены порядка элементов в нашей плиточной раскладке, которая вот вот уже будет сделана как мы хотим. Свойство order довольно простое в использовании: если у вас два элемента и у одного стоит order: 1, а у другого order: 2, то элемент с order: 1 будет орендерен перед другим элементом, вне зависимости от их порядка в HTML исходнике.

В нашем случае, решение зависит от тонкостей спецификации order: что случится, если два или более элементов будут с одинаковым order? Какой из них будет первым? В этом случае, flexbox опирается на исходный код: тот элемент, который идет первым в исходном HTML коде, будет отрендерен перед другим элементом с таким же значением order. Это даёт нам возможность легко перегруппировать элементы в сетке таким образом, что мы сможем сменить порядок расстановки элементов с колоночного на строчный, всё ещё рендеря эти строки, как колонки, используя nth-child().

Посмотрите на табличку ниже. Чтобы получить рациональный порядок с использованием flex-direction: row, нам просто надо отрендерить элементы в стандартном порядке: 1, 2, 3, 4, 5, 6, и т.д.

Но если нам надо получить тот же порядок с использованием flex-direction: column, то тогда уже нам понадобится поменять сам порядок элементов, таким образом, чтобы он соответствовал порядку каждой колонки в таблице (а не каждой строки):

То есть первыми элементами в нашей флексбокс раскладке должны 1, 4, 7, 10. Эти элементы заполнят первую колонку, далее 2, 5, 8, 11 для второй колонки и 3, 6, 9, 12 для третьей и последней колонки. Тут нам на помощь приходит селектор nth-child(). Мы можем его использовать для того, чтобы выделить каждый третий элемент (3n), начиная с первого элемента (3n + 1) и выставить всем этим элементам одинаковое значение order:

.item:nth-child(3n+1) { order: 1; }

Этот селектор выставит order: 1 для элементов 1, 4, 7, 10, то есть всем первым колонкам. Другими словами, мы используем комбинацию nth-child() и order, чтобы изменить порядок элементов на их изначальном положении. Чтобы создать 2 и 3 колонки, мы изменим порядок у других элементов:

.item:nth-child(3n+1) { order: 1; }
.item:nth-child(3n+2) { order: 2; }
.item:nth-child(3n) { order: 3; }

Тут мы делаем три группы элементов: 1, 4, 7, 10 (3n+1) c order: 1, далее 2, 5, 8, 11 (3n+2) с order: 2 и 3, 6, 9, 12 (3). Всё вместе выдаёт нам такой порядок элементов 1, 4, 7, 10, 2, 5, 8, 11, 3, 6, 9, 12.

Такой подход создаст иллюзию того, что элементы вернулись к своему изначальному порядку, как если бы вы рассматривали их в порядке слева направо. Если бы мы визуально оценивали эту сетку по строкам, то первая строка включала бы по первому элементу с каждой группы (1, 2, 3), вторая строка содержала бы каждый второй элемент из каждой группы (4, 5, 6) и так далее и т.п. Используя эту технику мы можем создать плиточную раскладку с элементами, отрендеренными как колонки, но имеющими порядок как было бы в строчном варианте.

А как это влияет на переходы с использованием табов? Да никак. Свойство order меняет только визуальное представление объектов, а не порядок переходов по клавише TAB, так что в этом плане всё будет работать, как и предполагается.

Предотвращаем слияние колонок

Если у вас довольно много элементов в раскладке, то этот подход 100% приведёт к тому, что что-то пойдет не так. Мы делаем расчёт на то, что каждая “группа”, которую мы создали, будет отрендерена ровно как одна колонка в контейнере, но в реальности у элементов могут быть разные высоты и колонки с лихвой могут сливаться друг с другом. Первая колонка может быть длиннее, чем другие две, для примера, что привело бы к тому, что третья колонка начиналась бы в конце второй:

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

Мы можем пофиксить эту проблему, заставив колонки рестартиться в указанные моменты. Вообще, с флексбоксами нет простого способа для указания “этот элемент должен перейти на новую строку”, но мы можем достигнуть этого эффекта, добавив невидимые элементы, которые возьмут 100% высоты контейнера. Так как они требуют 100% высоты родителя, то они не поместятся в колонку вместе с любым другим элементом, создавая тем самым, переходы на новые строки.

Нам нужно вставить такие переходы в сетку и массив элементов, таким образом, чтобы у нас получилась такая последовательность: 1, 4, 7, 10,<переход>, 2, 5, 8, 11,<переход>, 3, 6, 9, 12. Мы можем использовать псевдоэлементы на контейнере, чтобы добавить такие переходы и мы можем выставить order на 2 обоим. Добавление псевдоэлемента :before, сделает его первым потомком контейнера, а добавление псевдоэлемента :after сделает его последним потомком того же контейнера, так что если мы выставим order: 2 каждому из них, то они станут первым и последним элементом “группы” с order: 2 (так как они находятся до и после других элементов): :before, 2, 5, 8, 11, :after.

/* Новые колонки разделители */
.container::before,
.container::after {
content: "";
flex-basis: 100%;
width: 0;
order: 2;
}

Я подсветил псевдоэлементы ниже, чтобы показать их эффект. Обратите внимание, что элемент 3 мог бы попасть во вторую колонку, но он отображается как первый элемент в последней:

Решение

И в конце вам надо убедиться в том, что ваш флекс-контейнер имеет высоту выше, чем ваша самая высокая колонка (чтобы все колонки поместились). Теперь всё это совместите и так вы сможете сделать настоящую трехколоночную плиточную раскладку (вот codepen).

.container {
display: flex;
flex-flow: column wrap;
align-content: space-between;
/* Вашему контейнеру нужна фиксированная высота
*
и он должен быть выше, чем самая высокая колонка. */
height: 600px;
}

.item {
width: 32%;
margin-bottom: 2%; /* Optional */
}

/* Перегруппировываем элементы в 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;
}

Ваш html должен выглядеть примерно так, где div это каждый элемент в сетке:

<div class="container">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
...
</div>

Работаем с более, чем тремя колонками

Чтобы создать плиточную раскладку с более, чем тремя колонками, нам надо сделать несколько вещей: адаптировать наш алгоритм сортировки, обновить ширину элементов и вставить переходы на новые строки вручную (вместо использования псевдоэлементов). Для быстрого доступа к конечным результатам я собрал список codepen’ов показывающих плиточную раскладку на флексах для 3, 4, 5 и 6 колонок.

До этого мы были ограничены созданием только двух псевдоэлементов с :before и :after, теперь нам нужно прибегнуть к ручному добавлению переходов на новые строки внутри контейнера (вам требуется на один элемент-разделитель меньше, чем количество колонок в вашей раскладке). Вы можете просто вставить их в конец контейнера и они будут отсортированы по соответствующим колонкам:

<div class="container">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
...
<span class="item break"></span>
<span class="item break"></span>
<span class="item break"></span>
</div>

Мы вставляем разделители колонок как span, для того, чтобы отсортировать их в независимости от элементов контента. Нам нужен способ сбросить счет после того, как мы достигнем элемент разделитель или неравномерное число элементов сделает старт первого разделителя после 3 колонки, к примеру. Селектор :nth-of-type выбирает элементы вне зависимости от их типа, так что мы можем разделить порядок элементов и разделители таким образом:

.item:nth-of-type(4n+1) { order: 1; }
.item:nth-of-type(4n+2) { order: 2; }
.item:nth-of-type(4n+3) { order: 3; }
.item:nth-of-type(4n) { order: 4; }

Разделители же, как и в предыдущем примере, возьмут всю высоту контейнера:

/*  Переход к новой колонке */
.break {
flex-basis: 100%;
width: 0;
margin: 0;
}

В общем, это создаст плиточную колонку с 4-мя колонками (вот codepen):

Вот полное решение для CSS такой плиточной раскладки с 4-мя колонками:

.container {
display: flex;
flex-flow: column wrap;
align-content: space-between;
height: 600px;
}

.item {
width:24%;
margin-bottom: 2%; /* опционально */
}

.item:nth-of-type(4n+1) { order: 1; }
.item:nth-of-type(4n+2) { order: 2; }
.item:nth-of-type(4n+3) { order: 3; }
.item:nth-of-type(4n) { order: 4; }

.break {
flex-basis: 100%;
width: 0;
margin: 0;
}

Этот способ создания плиточной раскладки только средствами CSS, точно не является самым надежным, гибким или защищенным от внезапных косяков со стороны разработчика, в отличие от такого же исполнения на JavaScript (например Masonry), но если вы не хотите полагаться на сторонние библиотеки только для того, чтобы создать раскладку, то я надеюсь, что эти советы будут вам полезны.

--

--

Stas Bagretsov

Надеюсь верую вовеки не придет ко мне позорное благоразумие. webdev/sports/books