Адаптивная flex-сетка на CSS: разбираем реализацию на атомы

от автора

Наверное, каждый, кто сталкивался с frontend`ом, хотя бы раз использовал адаптивную flex-сетку на N-ном количестве колонок. В данной статье мы не станем рассматривать область применения такого подхода, его плюсы и минусы, а разберем теорию и напишем собственное решение, с брейкпоинтами и настраиваемым спейсингом!

Данная статья, в первую очередь, будет полезна новичкам, однако надеюсь, что и опытные хабровчане найдут в ней что-то интересное. Для упрощения жизни, будем использовать SCSS, продублировав CSS «под спойлер».

Кратко о подходе с колонками

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

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

В популярных библиотеках часто встречается реализация на 12 колонках, которую мы будем использовать в этой статье в качестве «настройки по умолчанию». Однако количество колонок ограничено лишь вашей фантазией. Чем больше их количество, тем больше возможностей для детализации размеров, но стоит учитывать, что для каждого размера потребуется создать отдельный CSS-класс.

Теория

Условная схема того, что мы хотим получить на выходе, выглядит следующим образом:

Фактически, это наша «система координат». Количество колонок фиксировано и определяет размер каждого элемента относительно ширины всего контейнера. Количество рядов бесконечно и отражено на схеме, как очень условная единица, просто для того, чтобы визуализировать «дорожки», по которым будут перемещаться наши будущие айтемы. 

Пока что не будем акцентировать внимание на контейнере. С ним все просто – это базовый display: flex. Ключевую роль в этой задаче играют элементы – они то и будут принимать в себя необходимые размеры. Чтобы максимально детально разобрать решение, я попытаюсь построить «дорогу из мыслей» с подробным объяснением проблем, которые могут возникнуть в процессе реализации.

Впервые столкнувшись с задачей построения подобной сетки, я подумал о применении к элементам всего трёх CSS-свойств: flex-grow, flex-shrink и flex-basis, в конечном итоге приведенных к простому flex: 1, flex: 2, flex: 3, … flex: 12.

Это действительно сработает для создания простейшей сетки, работающей исключительно в рамках одного ряда.

Однако проблема кроется в том, чтобы ограничить то самое максимальное количество колонок на ряд. Если мы попытаемся добавить второй элемент с размером flex: 6 – он не перенесется на новый ряд. Вместо этого, пропорции просто будут пересчитаны 

Решением этой проблемы является вычисление ширины элемента в процентном соотношении. Если контейнер составляет 100%, то элемент с относительным размером 3/12 должен занимать 25% ширины.

И это действительно работает:

В итоге мы имеем простой flex-контейнер, в котором размещаем элементы, а каждый элемент имеет ширину в процентном соотношении. После чего просто добавляем контейнеру gap… 

Нет, это не сработает. На этом этапе мы сталкиваемся с особенностью процесса вычисления конечного размера для элемента. Дело в том, что когда браузер переведет наши проценты в окончательные px – он не учтет свойство gap, выставленное контейнеру. Из-за чего реальная картина будет выглядеть примерно так:

Решение проблемы с отступами:

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

И тут на помощь приходит формула 

width = 100% * $size / $columns - ($columns - $size) * ($column-gap / $columns)

где 

$size – количество колонок которое должен занимать элемент

$columns – общее количество колонок

$column-gap – расстояние между колонками.

Подробный разбор формулы:

  1. 100% * $size / $columns – вычисляем базовую ширину элемента в процентах относительно контейнера.

  2. ($columns - $size) – количество колонок, которые НЕ занимает элемент. То есть, если элемент занимает меньше колонок, чем всего в сетке, то эта часть определяет количество промежутков, которые будут между элементом и соседними элементами.

  3. ($column-gap / $columns) – это ширина одного промежутка между колонками, выраженная в доле от общей ширины контейнера.

  4. ($columns — $size) * ($column-gap / $columns) – получаем общую ширину промежутков, которые «отнимаются» от ширины элемента.

  5. Объединяя обе части, мы получаем, ширину элемента равную его доле от полной ширины контейнера с учетом промежутков между колонками.

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

Реализация (пишем код)

Основная идея заключается в том, чтобы реализовать набор CSS-классов, каждый из которых будет отвечать за свой размер элемента на определенном брейкпоинте (например .xs-12, .md-6). Такой подход позволит задавать адаптивные размеры, «переключая» размер элемента на разных экранах.

Определим SCSS map, для удобной работы c брейкпоинтами:

// SCSS  $breakpoints: (   xs: 0,   sm: 600,   md: 900,   lg: 1200,   xl: 1536, );

Далее добавим стили контейнера:

// SCSS  .grid-container {   --row-gap: 0px;   --column-gap: 0px;    box-sizing: border-box;   width: 100%;   display: flex;   flex-wrap: wrap;   gap: var(--row-gap) var(--column-gap); }

Как упоминалось ранее – это базовый flex-контейнер. Сразу добавим CSS-переменные --row-gap и --column-gap – они понадобятся позднее, для того, чтобы выставить спейсинг между элементами в том месте, где применяем контейнер.

Важно отметить, что CSS переменные (да, собственно, как и gap) не поддерживаются в IE11.

Реализуем SCSS mixin, который будет отвечать за генерацию размерных классов:

// SCSS  @mixin create-grid-item($columns) {   & {     box-sizing: border-box;     flex-grow: 0;     flex-basis: auto;      @each $breakpoint, $value in $breakpoints {       @media (min-width: #{$value}px) {         @for $size from 1 through $columns {           &.#{$breakpoint}-#{$size} {             width: calc(100% * $size / $columns - ($columns - $size) * (var(--column-gap) / $columns));           }         }       }     }   } }

Здесь мы:

  1. Создаем @mixin, принимающий количество колонок, которое хотим «видеть» в сетке.

  2. Задаем базовые стили для элемента.

  3. Проходясь циклом по ранее созданной мапе $breakpoints генерируем классы с возможными размерами элемента (от 1 до $columns). Для каждого брейкпоинта используем свой media-запрос. В качестве имени класса используем шаблон .#{$breakpoint}-#{$size} (<ключ_брейкпоинта>-<размер>).

  4. Применяем формулу, для вычисления ширины элемента. Стоит обратить внимание, что в ней мы используем CSS-переменную, которую ранее объявили на этапе создания стилей контейнера.

  5. Обертка & {} необходима для избежания прямого размещения деклараций CSS-свойств после at-rule (подробнее об этом можно прочитать в документации).

Далее используем @mixin create-grid-item в .grid-item:

// SCSS  .grid-item {   @include utils.create-grid-item(12); }

В результате этих манипуляций, для выбранных брейкпоинтов и числа колонок, получим следующий набор классов:

Скрытый текст
// screen width: > 0px .xs-1 .xs-2 .xs-3 .xs-4 .xs-5 .xs-6 .xs-7 .xs-8 .xs-9 .xs-10 .xs-11 .xs-12  // screen width: > 600px .sm-1 .sm-2 .sm-3 .sm-4 .sm-5 .sm-6 .sm-7 .sm-8 .sm-9 .sm-10 .sm-11 .sm-12  // screen width: > 900px .md-1 .md-2 .md-3 .md-4 .md-5 .md-6 .md-7 .md-8 .md-9 .md-10 .md-11 .md-12  // screen width: > 1200px .lg-1 .lg-2 .lg-3 .lg-4 .lg-5 .lg-6 .lg-7 .lg-8 .lg-9 .lg-10 .lg-11 .lg-12  // screen width: > 1536px .xl-1 .xl-2 .xl-3 .xl-4 .xl-5 .xl-6 .xl-7 .xl-8 .xl-9 .xl-10 .xl-11 .xl-12

Теперь мы можем использовать сгенерированные классы, чтобы построить сетку:

<div class="grid-container" style="--row-gap: 20px; --column-gap: 20px;">   <div class="grid-item xs-6"></div>   <div class="grid-item xs-6"></div>   <div class="grid-item xs-6"></div>   <div class="grid-item xs-6 md-8 lg-12"></div>   <div class="grid-item xs-6 md-4 lg-12"></div> </div>

Спасибо за внимание!

Не возьмусь выносить вердикт, что такое решение отлично подойдет для любого проекта, как минимум, из-за того, что достаточно часто, в больших проектах, уже используется библиотека компонентов (например, Material UI), поставляющая готовую flex-сетку «из коробки». Однако оно может оказаться неплохим вариантом для легковесных сайтов.

Ключевым недостатком подобной реализации, лично я, вижу генерацию большого количества CSS-классов. Для стандартных пяти брейкпоинтов, при стандартных 12-ти колонках, будет сгенерировано 60 классов. Однако для решения этой проблемы можно использовать различные инструменты постпроцессинга, например, purgecss.

Код из статьи, готовый для использования в проекте – тут (GitHub).

Обещанный CSS
.grid-container {   --row-gap: 0px;   --column-gap: 0px;   box-sizing: border-box;   width: 100%;   display: flex;   flex-wrap: wrap;   gap: var(--row-gap) var(--column-gap); }  .grid-item {   box-sizing: border-box;   flex-grow: 0;   flex-basis: auto; }  @media (min-width: 0px) {   .grid-item.xs-1 {     width: calc(8.3333333333% - 11 * var(--column-gap) / 12);   }   .grid-item.xs-2 {     width: calc(16.6666666667% - 10 * var(--column-gap) / 12);   }   .grid-item.xs-3 {     width: calc(25% - 9 * var(--column-gap) / 12);   }   .grid-item.xs-4 {     width: calc(33.3333333333% - 8 * var(--column-gap) / 12);   }   .grid-item.xs-5 {     width: calc(41.6666666667% - 7 * var(--column-gap) / 12);   }   .grid-item.xs-6 {     width: calc(50% - 6 * var(--column-gap) / 12);   }   .grid-item.xs-7 {     width: calc(58.3333333333% - 5 * var(--column-gap) / 12);   }   .grid-item.xs-8 {     width: calc(66.6666666667% - 4 * var(--column-gap) / 12);   }   .grid-item.xs-9 {     width: calc(75% - 3 * var(--column-gap) / 12);   }   .grid-item.xs-10 {     width: calc(83.3333333333% - 2 * var(--column-gap) / 12);   }   .grid-item.xs-11 {     width: calc(91.6666666667% - 1 * var(--column-gap) / 12);   }   .grid-item.xs-12 {     width: calc(100% - 0 * var(--column-gap) / 12);   } }  @media (min-width: 600px) {   .grid-item.sm-1 {     width: calc(8.3333333333% - 11 * var(--column-gap) / 12);   }   .grid-item.sm-2 {     width: calc(16.6666666667% - 10 * var(--column-gap) / 12);   }   .grid-item.sm-3 {     width: calc(25% - 9 * var(--column-gap) / 12);   }   .grid-item.sm-4 {     width: calc(33.3333333333% - 8 * var(--column-gap) / 12);   }   .grid-item.sm-5 {     width: calc(41.6666666667% - 7 * var(--column-gap) / 12);   }   .grid-item.sm-6 {     width: calc(50% - 6 * var(--column-gap) / 12);   }   .grid-item.sm-7 {     width: calc(58.3333333333% - 5 * var(--column-gap) / 12);   }   .grid-item.sm-8 {     width: calc(66.6666666667% - 4 * var(--column-gap) / 12);   }   .grid-item.sm-9 {     width: calc(75% - 3 * var(--column-gap) / 12);   }   .grid-item.sm-10 {     width: calc(83.3333333333% - 2 * var(--column-gap) / 12);   }   .grid-item.sm-11 {     width: calc(91.6666666667% - 1 * var(--column-gap) / 12);   }   .grid-item.sm-12 {     width: calc(100% - 0 * var(--column-gap) / 12);   } }  @media (min-width: 900px) {   .grid-item.md-1 {     width: calc(8.3333333333% - 11 * var(--column-gap) / 12);   }   .grid-item.md-2 {     width: calc(16.6666666667% - 10 * var(--column-gap) / 12);   }   .grid-item.md-3 {     width: calc(25% - 9 * var(--column-gap) / 12);   }   .grid-item.md-4 {     width: calc(33.3333333333% - 8 * var(--column-gap) / 12);   }   .grid-item.md-5 {     width: calc(41.6666666667% - 7 * var(--column-gap) / 12);   }   .grid-item.md-6 {     width: calc(50% - 6 * var(--column-gap) / 12);   }   .grid-item.md-7 {     width: calc(58.3333333333% - 5 * var(--column-gap) / 12);   }   .grid-item.md-8 {     width: calc(66.6666666667% - 4 * var(--column-gap) / 12);   }   .grid-item.md-9 {     width: calc(75% - 3 * var(--column-gap) / 12);   }   .grid-item.md-10 {     width: calc(83.3333333333% - 2 * var(--column-gap) / 12);   }   .grid-item.md-11 {     width: calc(91.6666666667% - 1 * var(--column-gap) / 12);   }   .grid-item.md-12 {     width: calc(100% - 0 * var(--column-gap) / 12);   } }  @media (min-width: 1200px) {   .grid-item.lg-1 {     width: calc(8.3333333333% - 11 * var(--column-gap) / 12);   }   .grid-item.lg-2 {     width: calc(16.6666666667% - 10 * var(--column-gap) / 12);   }   .grid-item.lg-3 {     width: calc(25% - 9 * var(--column-gap) / 12);   }   .grid-item.lg-4 {     width: calc(33.3333333333% - 8 * var(--column-gap) / 12);   }   .grid-item.lg-5 {     width: calc(41.6666666667% - 7 * var(--column-gap) / 12);   }   .grid-item.lg-6 {     width: calc(50% - 6 * var(--column-gap) / 12);   }   .grid-item.lg-7 {     width: calc(58.3333333333% - 5 * var(--column-gap) / 12);   }   .grid-item.lg-8 {     width: calc(66.6666666667% - 4 * var(--column-gap) / 12);   }   .grid-item.lg-9 {     width: calc(75% - 3 * var(--column-gap) / 12);   }   .grid-item.lg-10 {     width: calc(83.3333333333% - 2 * var(--column-gap) / 12);   }   .grid-item.lg-11 {     width: calc(91.6666666667% - 1 * var(--column-gap) / 12);   }   .grid-item.lg-12 {     width: calc(100% - 0 * var(--column-gap) / 12);   } }  @media (min-width: 1536px) {   .grid-item.xl-1 {     width: calc(8.3333333333% - 11 * var(--column-gap) / 12);   }   .grid-item.xl-2 {     width: calc(16.6666666667% - 10 * var(--column-gap) / 12);   }   .grid-item.xl-3 {     width: calc(25% - 9 * var(--column-gap) / 12);   }   .grid-item.xl-4 {     width: calc(33.3333333333% - 8 * var(--column-gap) / 12);   }   .grid-item.xl-5 {     width: calc(41.6666666667% - 7 * var(--column-gap) / 12);   }   .grid-item.xl-6 {     width: calc(50% - 6 * var(--column-gap) / 12);   }   .grid-item.xl-7 {     width: calc(58.3333333333% - 5 * var(--column-gap) / 12);   }   .grid-item.xl-8 {     width: calc(66.6666666667% - 4 * var(--column-gap) / 12);   }   .grid-item.xl-9 {     width: calc(75% - 3 * var(--column-gap) / 12);   }   .grid-item.xl-10 {     width: calc(83.3333333333% - 2 * var(--column-gap) / 12);   }   .grid-item.xl-11 {     width: calc(91.6666666667% - 1 * var(--column-gap) / 12);   }   .grid-item.xl-12 {     width: calc(100% - 0 * var(--column-gap) / 12);   } }


ссылка на оригинал статьи https://habr.com/ru/articles/893646/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *