Как‑то на одном проекте понадобилось красиво равномерно разместить небольшие блоки‑виджеты в контейнере на странице. Сложность в том, что эти блоки различаются, как по высоте, так и по ширине. При чём нужно учесть адаптивность вёрстки и динамическое изменение содержимого, как контейнера, так и самих элементов — виджетов. Собственно мои изыскания по этой теме и вылились в разработку собственного решения и эту статью, которые, я надеюсь, будут полезны читателям.
Опишем подробно условия/пожелания задачи:
как можно меньшие пустоты между элементами и ими и границами контейнера;
равномерное расположение пустот;
по возможности, вообще обойтись без 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 > .scrollHeight) и ширине (.scrollWidth > .offsetWidth) не будет устранено */
let savety = 1000;
while((node.scrollHeight > node.offsetHeight || node.scrollWidth > node.offsetWidth) && savety){
node.style.height = parseInt(node.style.height) + step + 'px';
savety--;
}
Примечание. В принципе можно и не заморачиваться и начинать высоту с ноля. Дело в облегчении работы браузера, хотя на сколько именно в среднем оно благотворно сказывается на производительности трудно сказать, и уж точно конечный пользователь не заметит разницы.
Остальной код не буду здесь приводить, ибо банальность с обходом 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. Стоит так же упомянуть о известной подключаемой библиотеке, решающей как раз данную задачу, но, к сожалению, совсем вылетело из головы её название и нагуглить не смог. Там внутренние блоки размещались и позиционировались абсолютно. Если кто-то знает, пожалуйста напишите в комментариях.
P.P.S. На случай, если кому-то важно, статья и код писались без использования ИИ.
P.P.P.S. Телеграмм-канала у меня нет, так что подписываться некуда, извините.
Комментарии (10)

SDesya74
10.11.2025 11:20Есть способ размещения блоков различной высоты, как в пинтересте, называется masonry. Вроде бы была экспериментальная фича для него прямо в CSS, но без него делается скриптами тоже

sfi0zy
10.11.2025 11:20Стоит так же упомянуть о известной подключаемой библиотеке, решающей как раз данную задачу, но, к сожалению, совсем вылетело из головы её название и нагуглить не смог
Masonry. В свое время одноименную штуку в CSS пытались протолкнуть, но как-то не зашло.
Не совсем понятно, зачем этот инструмент переизобретать. Он как 10 лет назад решал свою задачу, так и продолжает ее решать.

admtoha Автор
10.11.2025 11:20Точно. Это та штука.
Благодарю, что напомнили.
На тот момент, когда решал эту проблему, эта библиотека мне показалась слишком тяжеловесной. Там, если мне не изменяет склероз, позиция каждого блока высчитывалась и размещение было через абсолютное позиционирование. Что для динамического изменения содержимого - сомнительно.
Современная версия, на первый взгляд, оно там вообще вроде через canvas, как-то реализовано....
P.S. Без специальных средств сайт проекта не открывается. Дожили...

ImagineTables
10.11.2025 11:20А прикладная задача какая была, если не секрет? Виджеты распределять по панели?

admtoha Автор
10.11.2025 11:20Да, виджеты на панели. С возможностью добавления / удаления.
В ленту их располагать в один ряд - слишком расточительно с точки зрения использования полезного пространства. Стандартизировать размер для всех - не получается, слишком разное содержимое.

ImagineTables
10.11.2025 11:20Ясно, спасибо. Лично я для виджетов предпочитаю олдскульный грид + перетаскивание.
sasha_solo
Все равно неровно получилось) нерешаемая задача. За отсутствие ии - респект
admtoha Автор
Идеал недостижим, но это не значит, что к нему нельзя стремиться.
P.S. Моя первая статья на Хабре и первый коммент под ней ^_^