Как‑то на одном проекте понадобилось красиво равномерно разместить небольшие блоки‑виджеты в контейнере на странице. Сложность в том, что эти блоки различаются, как по высоте, так и по ширине. При чём нужно учесть адаптивность вёрстки и динамическое изменение содержимого, как контейнера, так и самих элементов — виджетов. Собственно мои изыскания по этой теме и вылились в разработку собственного решения и эту статью, которые, я надеюсь, будут полезны читателям.
Опишем подробно условия/пожелания задачи:
как можно меньшие пустоты между элементами и ими и границами контейнера;
равномерное расположение пустот;
по возможности, вообще обойтись без JavaScript кода, либо сделать его участие минимальным;
адаптивность под разные размеры экранов и влияние других нод на странице вокруг контейнера;
поддержка динамической адаптивности под изменения (размеры, содержимое) страницы, контейнера и элементов, а так же создания/добавления/удаления таких контейнеров/элементов что называется «на лету».
Интерактивную демонстрацию всех приведённых в статье примеров можно посмотреть здесь.
"Дедовский" метод "float: left"

.container_float_left{ overflow: auto; > *{ float: left; } &::after{ clear: both; } }
Как видим, элементы распределяются по строкам, отсюда вытекают особенности. Вертикальные пустоты между элементом в строке определяется разницей между им и самым высоким блоком в той же строке. Так же возможна выделяющаяся по размеру горизонтальная пустота между последними элементом и правой границей контейнера.
По условиям/пожеланиям получаем:
величина пустот: если разница между блоками не большая, то +/- подходит, иначе - нет;
равномерность пустот: нет, равномерности тут не наблюдаем;
без JS: подходит;
адаптивность: подходит;
динамическая адаптивность: подходит.
Метод "flex-flow: row wrap"

.container_flex_row_wrap{ display: flex; flex-flow: row wrap; justify-content: space-evenly; }
Блоки распределяются так же по строкам, как и в предыдущем методе, но, благодаря "justify-content: space-evenly", пустые расстояния между соседними элементами в строке равные.
По условиям/пожеланиям получаем:
величина пустот: если разница между блоками не большая, то +/- подходит, иначе - нет;
равномерность пустот: частично, соблюдается только между соседними элементами в строке;
без JS: подходит;
адаптивность: подходит;
динамическая адаптивность: подходит.
Метод "column-count: 4"

.container_columns{ column-count: 4; column-gap: 0; > *{ display: inline-block; } }
На первый взгляд, смотрится, как то, что нужно. Минимальные пустоты, так как распределение блоков идёт по столбцам, а не по строкам. Правда, равномерности в промежутках между столбцами уже нет. В минусы можно отнести то, что количество столбцов нужно прописывать вручную, так что адаптивность можно обеспечить разве что правилами @media для каждого случая размера экрана. О поддержке динамических изменений говорить не приходится.
Стоит отметить, что если не делать внутренние блоки строковыми, происходит их разрезание на части, которые разносятся на разные столбцы.
По условиям/пожеланиям получаем:
величина пустот: подходит;
равномерность пустот: частично, соблюдается только между блоками в столбцах;
без JS: подходит;
адаптивность: подходит с условием использования правил @media для каждого случая размера экрана;
динамическая адаптивность: нет.
Мой метод "flex-flow: column wrap" + js-скрипт

В основе лежит режим разметки "flex" со свойством "flex-flow: column wrap". Дело в том, что без ограничения контейнера по высоте мы получаем просто один столбец со всеми вложенными элементами. Однако, установив точную высоту, мы получаем как-раз то, что нужно, что наблюдаем на иллюстрационном изображении. Как не трудно догадаться, скрипт занимается как раз определением этой высоты блока, а так же следит за изменениями страницы, запуская это переопределение по необходимости.
Суть в том, чтобы установив примерную (заведомо меньшую реальной) высоту контейнера, постепенно в цикле по небольшим шагам увеличивать оную (высоту) до тех пока не будет устранено переполнение. По итогу это и будет искомая оптимальная величина. Наличие переполнения по высоте определяется так: .scrollHeight > .scrollHeight. По ширине: .scrollWidth > .offsetWidth.
Фрагмент кода с непосредственными вычислением и установкой высоты контейнера:
/* node - блок-контейнер */ const step = 10; // шаг подбора высоты let square = 0, // общая площадь всех внутренних блоков max_height = 0; // максимальна высота блока /* обход всех дочерних елементов с заполнением переменных, объявленных выше */ [...node.childNodes].forEach(child => { if(child.nodeName === '#text') return; square += child.offsetHeight * child.offsetWidth; if(child.offsetHeight > max_height) max_height = child.offsetHeight; }); /* вычисляем стартовую высоту */ let start_height = square / node.offsetWidth; if(start_height < max_height) start_height = max_height; /* устанавливаем стартовую высоту */ if(!isNaN(start_height)) node.style.height = start_height + 'px'; /* цикл увеличения высоты контейнера "node" проводит итерации до тех пор , пока переполнение по высоте (.scrollHeight > .offsetHeight) и ширине (.scrollWidth > .offsetWidth) не будет устранено */ let safety = 1000; while((node.scrollHeight > node.offsetHeight || node.scrollWidth > node.offsetWidth) && safety){ node.style.height = parseInt(node.style.height) + step + 'px'; safety--; }
Примечание. В принципе можно и не заморачиваться и начинать высоту с ноля. Дело в облегчении работы браузера, хотя на сколько именно в среднем оно благотворно сказывается на производительности трудно сказать, и уж точно конечный пользователь не заметит разницы.
Остальной код не буду здесь приводить, ибо банальность с обходом DOM дерева с поиском целевых нод и отслеживание за изменениями страницы и участвующих узлов с помощью "обзёрверов", без каких-то подводных камней. Хотя, нужно заметить не очевидную особенность: при наличие прокрутки страницы �� динамическом изменении целевой ноды может произойти резкий неприятный сдвиг прокрученной области (как-будто без причины). Фиксится просто:
находим родительский HTML-элемент с прокруткой;
запоминаем текущее положение скролла;
проводим непосредственную установку новой высоты для целевого блока;
возвращаем положение текущей прокрутки с помощью ранее сохранённой координаты.
Буквально 4 строки кода:
const get_scroll_parent = node => node.parentNode?.scrollTop ? node.parentNode : node.parentNode ? get_scroll_parent(node.parentNode) : null, scroll_parent = get_scroll_parent(node), // находим родителя с прокруткой, если он есть current_scroll = scroll_parent ? scroll_parent.scrollTop : null; // запоминаем текущее положение скролла ... /* Подгоняем высоту блока */ ... /* исправляем прокрутку, если требуется */ if(current_scroll && current_scroll !== scroll_parent.scrollTop) scroll_parent.scrollTo({top: current_scroll, behavior: 'instant'});
Так как вмешательство скрипта минимально у нас имеется арсенал нативных CSS-свойств для кастомизации.
Однако, отсутствует возможность пользоваться только следующими свойствами:
display;
flex-flow;
flex-direction;
flex-wrap;
flex-shrink;
flex-grow.
Как пользоваться:
Скачиваем скрипт и подключаем к своей странице либо копируем весь код из файла и размещаем в теге "script" внутри блока "head" либо "body" нашего html.
Активируем целевые блоки-контейнеры с помощью атрибута "data-flex-size-fix".
Подключение скрипта (на всякий случай):
<script language="JavaScript" src="./flex_size_fix.js"></script>
Инициализация целевых блоков-контейнеров с помощью атрибута "data-flex-size-fix":
... <div data-flex-size-fix> ... </div> ...
По условиям/пожеланиям получаем:
величина пустот: подходит;
равномерность пустот: подходит;
без JS: нет, но минимальное вмешательство;
адаптивность: подходит;
динамическая адаптивность: подходит.
Примечание. Ломал голову о звучном названии скрипта. Пришёл к выводу, что тот набор слов через тире (Flex-Size-Fix) в принципе отражает всю суть.
P.S. Стоит так же упомянуть о известной подключаемой библиотеке, решающей как раз данную задачу, но, к сожалению, совсем вылетело из головы её название и нагуглить не смог. Там внутренние блоки размещались и позиционировались абсолютно. Если кто-то знает, пожалуйста напишите в комментариях.
UPD. Эта библиотека - Masonry. Спасибо за напоминание sfi0zy.
P.P.S. На случай, если кому-то важно, статья и код писались без использования ИИ.
P.P.P.S. Телеграмм-канала у меня нет, так что подписываться некуда, извините.
