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

Опишем подробно условия/пожелания задачи: 

  1. как можно меньшие пустоты между элементами и ими и границами контейнера;

  2. равномерное расположение пустот;

  3. по возможности, вообще обойтись без JavaScript кода, либо сделать его участие минимальным;

  4. адаптивность под разные размеры экранов и влияние других нод на странице вокруг контейнера;

  5. поддержка динамической адаптивности под изменения (размеры, содержимое) страницы, контейнера и элементов, а так же создания/добавления/удаления таких контейнеров/элементов что называется «на лету».

Интерактивную демонстрацию всех приведённых в статье примеров можно посмотреть здесь.

"Дедовский" метод "float: left"

float: left
float: left
.container_float_left{
    overflow: auto;
    > *{
        float: left;
    }
    &::after{
        clear: both;
    }
}

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

По условиям/пожеланиям получаем:

  1. величина пустот: если разница между блоками не большая, то +/- подходит, иначе - нет;

  2. равномерность пустот: нет, равномерности тут не наблюдаем;  

  3. без JS: подходит;

  4. адаптивность: подходит;

  5. динамическая адаптивность: подходит.

Метод "flex-flow: row wrap"

flex-flow: row wrap
flex-flow: row wrap
.container_flex_row_wrap{
    display: flex;
    flex-flow: row wrap;
    justify-content: space-evenly;
}

Блоки распределяются так же по строкам, как и в предыдущем методе, но, благодаря "justify-content: space-evenly", пустые расстояния между соседними элементами в строке равные.  

По условиям/пожеланиям получаем:

  1. величина пустот: если разница между блоками не большая, то +/- подходит, иначе - нет;

  2. равномерность пустот: частично, соблюдается только между соседними элементами в строке;  

  3. без JS: подходит;

  4. адаптивность: подходит;

  5. динамическая адаптивность: подходит.

Метод "column-count: 4"

column-count: 4
column-count: 4
.container_columns{
    column-count: 4;
    column-gap: 0;
    > *{
        display: inline-block;
    }
}

На первый взгляд, смотрится, как то, что нужно. Минимальные пустоты, так как распределение блоков идёт по столбцам, а не по строкам. Правда, равномерности в промежутках между столбцами уже нет. В минусы можно отнести то, что количество столбцов нужно прописывать вручную, так что адаптивность можно обеспечить разве что правилами @media для каждого случая размера экрана. О поддержке динамических изменений говорить не приходится.

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

По условиям/пожеланиям получаем:

  1. величина пустот: подходит;

  2. равномерность пустот: частично, соблюдается только между блоками в столбцах;  

  3. без JS: подходит;

  4. адаптивность: подходит с условием использования правил @media для каждого случая размера экрана;

  5. динамическая адаптивность: нет.

Мой метод "flex-flow: column wrap" + js-скрипт

flex-flow: column wrap + Flex-Size-Fix
flex-flow: column wrap + Flex-Size-Fix

В основе лежит режим разметки "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 дерева с поиском целевых нод и отслеживание за изменениями страницы и участвующих узлов с помощью "обзёрверов", без каких-то подводных камней. Хотя, нужно заметить не очевидную особенность: при наличие прокрутки страницы и динамическом изменении целевой ноды может произойти резкий неприятный сдвиг прокрученной области (как-будто без причины). Фиксится просто:

  1. находим родительский HTML-элемент с прокруткой; 

  2. запоминаем текущее положение скролла; 

  3. проводим непосредственную установку новой высоты для целевого блока;

  4. возвращаем положение текущей прокрутки с помощью ранее сохранённой координаты. 

Буквально 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>
...

По условиям/пожеланиям получаем:

  1. величина пустот: подходит;

  2. равномерность пустот: подходит

  3. без JS: нет, но минимальное вмешательство;

  4. адаптивность: подходит;

  5. динамическая адаптивность: подходит.

Примечание. Ломал голову о звучном названии скрипта. Пришёл к выводу, что тот набор слов через тире (Flex-Size-Fix) в принципе отражает всю суть. 

P.S. Стоит так же упомянуть о известной подключаемой библиотеке, решающей как раз данную задачу, но, к сожалению, совсем вылетело из головы её название и нагуглить не смог. Там внутренние блоки размещались и позиционировались абсолютно. Если кто-то знает, пожалуйста напишите в комментариях.

P.P.S. На случай, если кому-то важно, статья и код писались без использования ИИ.

P.P.P.S. Телеграмм-канала у меня нет, так что подписываться некуда, извините.

Комментарии (10)


  1. sasha_solo
    10.11.2025 11:20

    Все равно неровно получилось) нерешаемая задача. За отсутствие ии - респект


    1. admtoha Автор
      10.11.2025 11:20

      Идеал недостижим, но это не значит, что к нему нельзя стремиться.

      P.S. Моя первая статья на Хабре и первый коммент под ней ^_^


  1. SDesya74
    10.11.2025 11:20

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


  1. sfi0zy
    10.11.2025 11:20

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

    Masonry. В свое время одноименную штуку в CSS пытались протолкнуть, но как-то не зашло.

    Не совсем понятно, зачем этот инструмент переизобретать. Он как 10 лет назад решал свою задачу, так и продолжает ее решать.


    1. admtoha Автор
      10.11.2025 11:20

      Точно. Это та штука.

      Благодарю, что напомнили.

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

      Современная версия, на первый взгляд, оно там вообще вроде через canvas, как-то реализовано....

      P.S. Без специальных средств сайт проекта не открывается. Дожили...


  1. ImagineTables
    10.11.2025 11:20

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


    1. admtoha Автор
      10.11.2025 11:20

      Да, виджеты на панели. С возможностью добавления / удаления.

      В ленту их располагать в один ряд - слишком расточительно с точки зрения использования полезного пространства. Стандартизировать размер для всех - не получается, слишком разное содержимое.


      1. ImagineTables
        10.11.2025 11:20

        Ясно, спасибо. Лично я для виджетов предпочитаю олдскульный грид + перетаскивание.


  1. svkozlov
    10.11.2025 11:20

    ребята, есть же уже готовое решение, применяется при паковании текстур в атлас


    1. admtoha Автор
      10.11.2025 11:20

      Можно поподробнее?