Приключения Masonry-раскладки в CSS

«Masonry-раскладка», она же «плиточная верстка», она же «раскладка кирпичиками», она же «Cascading Grid», она же — вернее, один из ее вариантов — «верстка как у Pinterest», она же… в общем, задача верстки такого типа макетов известна верстальщикам очень давно, под многими именами. Раньше на чистом CSS она до конца не решалась. Можно было добиться внешне похожего результата для частных случаев, но какой-то нюанс ускользал. На практике приходилось использовать JS-библиотеки — прежде всего Masonry, написанную Дейвом ДеСандро и, собственно, давшую название такой раскладке.

И вот в Рабочей группе по CSS появилось предложение, а спустя считанные месяцы — и его экспериментальная реализация, которую уже можно пощупать в Firefox Nightly/Beta за флагом. Кроме понятной радости, новинка успела вызвать немало путаницы и споров. Попробуем разобраться.

Постановка задачи

Причем тут «масоны»? 🙂

Сходство названия библиотеки и легендарного тайного общества не случайно: оба — от слова «каменщик». Общая логика masonry-верстки похожа на логику каменной кладки: берем «кирпичик» и укладываем его вплотную к предыдущим.

Но где хоть вскользь упомянуты масоны, там не обходится без путаницы:). Как оказалось, при слове «masonry-раскладка» разные люди представляют себе очень разные вещи — и часто по умолчанию каждый считает свой вариант единственно возможным!

Макеты и библиотеки

Одни представляют себе «каменную кладку» как набор совершенно произвольных блоков, которые надо как можно плотнее прижать друг к другу — типа как на этом фото:

Плотная кладка из каменных блоков разной формы

В вебе форма блоков (пока) ограничена прямоугольниками, но их размеры теоретически могут быть любыми. Справедливости ради, это задача скорее для другой библиотеки ДеСандро — Packery. Ниже скриншот с ее сайта («шапка» там работает как наглядное демо, все эти темные прямоугольники — отдельные анимируемые блоки):

Скриншот сайта библиотеки Packery

У ДеСандро есть и третья библиотека для раскладки блоков — Isotope. Автор разъясняет, что задача Packery — именно упаковка произвольных блоков, Masonry создает masonry-макеты (логично!:), а у Isotope основной упор на интерактивность (перетягивание и т.п.), и она может оперировать разными схемами раскладки (в т.ч. «masonry»). Но все три библиотеки во многом схожи и в какой-то мере даже взаимозаменяемы. Пять лет назад Masonry помогла нам решить задачу плотной упаковки, а первый же пример для Packery называется… «masonry-галерея».

Но чаще всего под «masonry-раскладкой» понимается макет, где по горизонтали блоки вписываются в некую регулярную сетку (занимают целое число колонок, опционально с разделительными интервалами), а по вертикали — как получится. Первым на ум приходит, конечно же, Pinterest, где все блоки одинаковой ширины (в одну колонку). Многие уже считают слово «Pinterest» синонимом для «masonry» и даже не вспоминают про другие варианты. Но возможности библиотеки куда шире, и в ее онлайн-документации Дейв приводит множество симпатичных примеров вроде вот такого:

Пример masonry-раскладки из документации к библиотеке Masonry

Два разных порядка блоков

Именно в нем ключевое отличие «masonry-раскладки» от обычной многоколоночной: колонки заполняются блоками не последовательно, по вертикали, а «параллельно», по горизонтали. Но в библиотеке Masonry есть два режима, переключаемые параметром horisontalOrder:Два варианта порядка блоков в Masonry: по умолчанию они попадают в наименее заполненную колонку, а с параметром horizontalOrder заполняют колонки по порядку

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

А с включенным параметром блоки, наоборот, соблюдают порядок по горизонтали (первый блок — в первую колонку, второй — во вторую… n-й — в n-ю, n+1-й — снова в первую, n+2-й — снова во вторую, и т.д.). Поскольку высота блоков разная и заранее неизвестная, получающиеся «строки» быстро начинают «скакать» вверх-вниз, а высота колонок может стать неравномерной. Но раз автор добавил такой режим, наверное, для чего-то он бывает нужен…

Промежуточный итог: варианты

Получается, что masonry-раскладка — не одна конкретная схема, а множество разных, хоть и родственных, задач. Как минимум:

  1. Плотная, насколько можно, упаковка блоков произвольного размера, без привязки к какой-либо сетке (как у Packery);
  2. Вписывание блоков в сетку по одной оси (с кратным шагом) и плотная упаковка по другой (как в примерах от Masonry);
  3. Заполнение колонок по доступному месту (Masonry по умолчанию);
  4. Заполнение колонок по порядку по горизонтали (Masonry c horizontalOrder=true);
  5. … и т.д. (ждем ваших примеров в комментариях!)

Подходы к решению

CSS-колонки

Это первое, что приходит на ум при виде макета а-ля Pinterest. Визуально — практически что надо. Куча статей в интернете прямо представляет их как «masonry на чистом CSS». Но главная загвоздка — порядок элементов совсем не тот, что мы ищем. Динамическую подгрузку так не сделать. И еще одна загвоздка — колонки нельзя объединять, разве что сразу все (по крайней мере, пока). Впрочем, раз порядок не подходит, это уже мелочи…

Флексбоксы

С flex-flow: column wrap; они похожи на мультиколонки, но у них есть мощный козырь для управления порядком: свойство order. К этому бы еще возможность явно заставлять колонки флекс-элементов переноситься (в Firefox ее уже можно эмулировать благодаря багу) — и получается решение 4-го варианта задачи, заполнение колонок по порядку. Ориоль Брюфо даже показал его на примере:

#flex-container { display: flex; flex-flow: column wrap;
} #flex-container > :nth-child(3n + 1) { order: 1; } /* колонка 1 */
#flex-container > :nth-child(3n + 2) { order: 2; } /* колонка 2 */
#flex-container > :nth-child(3n + 3) { order: 3; } /* колонка 3 */ #flex-container > :nth-child(-n + 3) { break-before: flex; /* так должно быть по текущей спеке */ break-before: always; /* так работает в Firefox, хотя не должно:) */
}

Но… слишком много «но». Это лишь один вариант задачи, не самый востребованный. Про объединение колонок придется забыть — одномерные флексбоксы такого не умеют. Как и про «отзывчивость» — для каждого числа колонок придется отдельно прописывать n селекторов: громоздко и негибко.

А может… старые добрые флоаты?

Их поведение часто очень напоминает вариант 1 (как в заставке от Packery): стремятся расположиться как можно выше, а если нет места — ищут его рядом по горизонтали:

See the Pen
abvqOVz
by Ilya Streltsyn (@SelenIT)
on CodePen.

Вот только ищут они его лишь в одном направлении, оставляя пропуски. Например, здесь для блочка «Title 7» с запасом хватило бы места под «Title 2», но вернуться туда, «поднырнув» под ранее идущий «Title 5», алгоритм ему не разрешает.

В обсуждении на гитхабе это заметил Роберт Уташи и предложил новое значение top left для свойства float. Я бы скорее назвал его left dense — по аналогии с grid-auto-flow: row dense в гридах, которое отличается от обычного row как раз тем, что «заполняет пропуски». Но это уже детали…

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

Или всё-таки гриды?

На сайте Masonry в самом верху прямо сказано: «Masonry is a JavaScript grid layout library». То есть сам автор считает свою библиотеку инструментом именно для сеточных раскладок!

Стандартные средства CSS-гридов почти решают задачу в варианте 2 (строго заданная сетка по горизонтали, плотная упаковка по контенту по вертикали):

See the Pen
Masonry (резиновые колонки) на CSS Grid
by Ilya Streltsyn (@SelenIT)
on CodePen.

Увы, ключевую деталь — высоту блоков в терминах занимаемых ими грид-ячеек — приходится задавать руками (или скриптом).

Для простейшего («пинтерестовского») варианта гриды, да еще с «довеском», могут показаться перебором. Но именно их двумерная природа, в сочетании с уже упоминавшимся ключевым словом dense для способа авторазмещения, уже сейчас позволяет заполнять колонки равномерно, как чаще всего и нужно. И нам доступны все преимущества грид-раскладки (все способы задания ширины колонок, auto-fill, column-gap и т.д.) по горизонтальной оси.

Так что основания взять за основу своей реализации именно гриды у Мэтса Палмгрена и его коллег из Мозиллы были.

Текущая экспериментальная реализация

Примеры

Для просмотра примеров нужен Firefox Nightly, Developer Preview или Beta 77-й версии и выше. Чтобы они заработали, нужно сначала включить настройку layout.css.grid-template-masonry-value.enabled в about:config. Особенно хочется отметить:

Новые свойства и значения

У новинки по сути всего 2 ключевых нововведения:

  1. Ключевое слово masonry для свойств grid-template-rows/-columns, включающее masonry-поведение по соотв. оси;
  2. Новое свойство masonry-auto-flow, управляющее порядком вывода элементов.

Значение этого свойства может состоять из двух частей. Первая часть по сути — аналог параметра horisontalOrder из библиотеки Masonry: ключевое слово next в нем переключает порядок с дефолтного (по свободному месту) на наполнение колонок по порядку. А вторая часть отвечает за поведение в том случае, если для некоторых блоков явно задана привязка к конкретным колонкам. По умолчанию такие элементы размещаются в первую очередь, а остальные заполняют оставшееся место. Но это можно отключить и заставить их все располагаться в порядке единой «живой очереди» (по-моему, случай довольно специфический, но мало ли…).

Кстати, свойство order здесь тоже работает и влияет на изначальный порядок элементов, в котором они «разливаются» по колонкам. Правда, с ним особо велик риск совсем запутаться, ведь порядок здесь и так не всегда нагляден.

Возражения и контраргументы

Сложность

Гриды и так сложны — куча новых свойств с похожими названиями, еще большая куча новых значений, бездна возможных сочетаний, масса правил и тонны исключений из них… А еще подсетки (сабгриды), сами только-только проникающие в браузеры. А ещё взаимодействие с другими CSS-модулями, например, теми же колонками. Плюс возможные будущие расширения (например, непрямоугольные области или несколько гридов на одном элементе). Как это всё «подружится» ещё и с masonry-механизмом?

Но зато благодаря инструментам гридов (все способы задания ширины колонок и т.д.) теперь можно верстать такое, чего даже исходная Masonry не умела — см. второй пример Джен Симмонс. И это должно работать быстро (нативная возможность браузера, да еще размеры можно зафиксировать на уровне контейнера и не пересчитывать по мере подгрузки контента). Жалко ведь от этих возможностей отказываться. И не дублировать же одну и ту же функциональность в разных модулях?..

Это не грид

Фактически, мы отнимаем у грида его главную особенность — упорядоченную структуру грид-полос, основу собственно сетки. Пусть лишь по одной оси, но всё же. Задавать display:grid для того, чтобы в итоге получить не грид — как-то не очень логично. Особенно это раздражало тех, у кого «masonry» ассоциировалось исключительно с «как Pinterest»: какой же это грид, это больше похоже на флексбоксы с необычным порядком элементов.

Но как быть с элементами, охватывающими две и более колонок? Для Эрика Мейера этого аргумента оказалось достаточно, чтобы изменить мнение: да, это не совсем грид, но от флексбоксов оно еще дальше.

Нелогичное поведение

Эта двойственность — «вроде грид, а вроде и нет» — может быть также источником путаницы и неожиданностей, особенно для новичков. Например, в гриде с grid-template-rows: masonry, по идее, вообще не должно быть грид-рядов (только колонки), но на практике в нем оказывается единственный «волшебный» первый как-бы-явный ряд. Это хорошо видно в примере Рейчел (если «поиграть» со значениями grid-row для элемента .box).

Когда я «ковырялся» в этом примере, мне вдруг пришло на ум: а что, если грид не ломать, оставить его гридом с рядами и колонками, но разрешить элементам игнорировать линии при авторазмещении? Т.е. не grid-template-*: masonry, а grid-auto-flow: row masonry — этакий «совсем уж радикальный dense»:). Тогда ведь можно будет творить вообще невероятные вещи, сочетая нормальные грид-элементы на фиксированных позициях с «обтекающими» их masonry-элементами… ух! Мысль слишком безумная, чтобы сразу предлагать на суд Рабочей группы по CSS, решил вот сначала посоветоваться с вами:)

Итого

Итоги подводить пока рано. В текущей реализации masonry на базе гридов есть очевидные минусы, но и заманчивые плюсы. Обсуждение вовсю идет, и иногда отличные идеи спонтанно возникают в процессе. Так что подключайтесь, экспериментируйте, и не упускайте возможности прямо сейчас повлиять на будущее CSS!

P.S. Это тоже может быть интересно: