Нетривиальная расстановка элементов на flexbox без media-запросов

Казалось бы, какой пост может быть о CSS Flexbox в 2019 году? Верстальщики уже несколько лет активно используют данную технологию, и все тайны должны быть разгаданы.

Однако, недавно у меня возникло стойкое ощущение, что нужно поделиться одним нетривиальным и, на мой взгляд, полезным приёмом, связанным с flexbox. Написать пост побудил тот факт, что ни один знакомый (из учеников, верстальщиков и просто людей, близких к web), не смог решить задачку, связанную с flexbox, хотя на это нужно всего 4-6 строк.

Итак, сначала постановка задачи, затем предыстория и, наконец, решение.

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


  1. На большом экране два элемента расположены горизонтально, при адаптивке на телефонах – вертикально.
  2. При этом на больших экранах они должны прижиматься к краям (как при justify-content: space-between), а на маленьких — стоять по центру (justify-content: center).

Казалось бы, что тут решать!? Но есть ещё одно условие: надо сделать это перестроение в автоматическом режиме – без media-запросов.

Зачем, спросите вы? К чему эти фокусы с запретом на media-запросы, зачем flexbox ради flexbox?

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



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

Ещё один яркий пример: логотип и меню в шапке. Ширина лого известна, а вот с меню — проблема. Ведь часто мы делаем вёрстку на формально заполненном шаблоне, а в реальности меню будет формироваться из админки сайта. Сколько пунктов – никто не знает, следовательно, не ясно, на какой ширине делать перестроение.



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

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

Способ 1 просто неудобен, способ 2 будет более-менее нормально смотреться для примера с заголовками и датами, но будет просто ужасен для логотипа и меню.

Итак, постановка задачи. Как на flexbox без media-запросов организовать идеальное поведение элементов: визуально расположить их по краям большого экрана и отцентровать при соударении? (Ну или почти идеальное, из-за использования text-align и flex-grow вылезут небольшие ограничения, которые легко решаются с помощью дополнительной обёртки)

Предыстория


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

Для достижения идеального поведения всегда чего-то не хватало. Flex-grow поможет на одном экране, навредит на другом. Margin: auto и text-align помогают, но и их не хватает для достижения результата.

В итоге я, как думают и большинство верстальщиков, решил, что flex просто не умеют так делать. И успешно забыл данную задачу, научившись обходиться другими средствами и приёмами при вёрстке.

Озарение пришло летом 2019, когда, разбирая домашнее задание на курсе по вёрстке, я увидел совершенно не по делу применённый flex-grow: 0.5. В том примере жадность 0.5 не сильно помогала решить задачу, однако, я сразу почувствовал, что вот он – секретный ингредиент, которого всегда не хватало. Не то, чтобы я не знал, что grow может быть дробным. Просто в голову не приходило, что 0.5 может помочь достичь идеальной расстановки! Теперь это кажется очевидным, ведь flex-grow: 0.5 – это захват ровно половины свободного пространства

В общем, увидев flex-grow: 0.5, я за 10 минут полностью решил задачу расстановки элементов, которую с 2014 года считал нерешаемой. Увидев, что код получился очень простой и лаконичный, я решил, что изобрёл давно известный велосипед. Но всё-таки несколько дней назад я решил проверить, насколько хорошо он известен верстальщикам и запустил интерактив на своём youtube-канале по веб-разработке с 45 тыс. подписчиков.

Задал задачку, стал ждать. И её не решил никто из подписчиков. Не решили её и знакомые верстальщики. Крепло ощущение, что народ просто не знает, как идеально расположить элементы с помощью flex. Так и созрел данный пост. Ведь теперь мне кажется, что огромное количество верстальщиков считают, что так, как было описано в постановке задачи, расставить элементы без media-запросов просто нельзя.

Решение, которое мы рассмотрим ниже является вполне универсальным. Иногда оно требует создания дополнительной обёртки для центровки текста или правильного отображения фона. Но зачастую удаётся обойтись всего 6 строками CSS-кода без изменения структуры.

Базовое решение


За основу возьмём небольшой кусочек HTML. Это пример, который мы видели на самой первой gif-картинке. Дата и заголовок прижаты к краям, а при перескоке должны становиться по центру.

<div class="blog">
  <div class="blogItem">
    <div class="blogItem__cat">Article posted in: PHP Programming</div>
    <div class="blogItem__dt">21.10.2019</div>
  </div>
  <div class="blogItem">
    <div class="blogItem__cat">Article posted in: Javascript</div>
    <div class="blogItem__dt">22.10.2019</div>
  </div>
  <div class="blogItem">
    <div class="blogItem__cat">Article posted in: wft my programm don't work</div>
    <div class="blogItem__dt">23.10.2019</div>
  </div>
</div>

Опустим неинтересные стили, связанные с фонами, рамками и т.п. Главная часть CSS:

.blogItem {
  display: flex;
  flex-wrap: wrap;
}

.blogItem__cat {
  flex-grow: 0.5;
  margin-left: auto;
}

.blogItem__dt {
  flex-grow: 0.5;
  text-align: right;
}

Когда видишь этот код первый раз, может сложиться ощущение, что его написал не человек, а рандомный бредогенератор CSS-свойств! Поэтому нам обязательно нужно осознать, как это работает!

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



Изначально flex-элементы стоят рядом и получают размер по своему контенту. flex-grow: 0.5 отдаёт каждому элементу по половине свободного пространства, т.е. свободного пространства нет, когда они стоят рядом. Это безумно важно, потому что margin-left: auto вообще не работает, пока элементы помещаются на одной строке.

Первый элемент сразу стоит идеально, содержимое второго нужно прибить к правому краю с помощью text-align: right. В итоге мы получили аналог justify-content: space-between, вообще не написав justify-content.

Но ещё большие чудеса начинаются при перескоке элементов на отдельные строки. Margin-left auto отталкивает первый элемент на максимально возможное значение от левого края. Ну-ка, ну-ка, сколько там у нас пространства осталось? 100% минус размер элемента минус половина свободного от grow = половина свободного! Вот такая центровка!

Второй элемент стоит у левого края и занимает пространство, равное своему базовому размеру, плюс половина свободного. В это трудно поверить, но text-align: right отправит элемент идеально по центру!

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

Ключом к созданию такого решения является flex-grow: 0.5, без которого не удаётся отключить margin-[left-right] auto на больших экранах. Ещё один фантастический факт – justify-content вообще не используется. Тотальный парадокс ситуации заключается в том, что любая попытка выравнивания контента с помощью justify-content сломает нашу схему.

Абсолютно такими же свойствами мы можем поставить рядом и более крупные и сложные элементы, например, меню и логотип – ссылка на песочницу. Ещё раз подчёркиваю, что меню всё равно придётся превращать в кнопку, но теперь мы гораздо меньше переживаем по поводу того, что оно слетит вниз раньше нужного. Ведь даже слетев, меню смотрится очень хорошо!

Улучшенное решение


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



Если бы заголовок изначально был втором элементом (располагался справа от даты), то тогда на него действовал бы text-align: right, из-за которого текст смотрелся бы ещё более странно. Кстати, и дате, и заголовку мы не можем поставить фон из-за flex-grow 0.5.

Однако это всё легко решается созданием дочернего элемента со свойствами

some-selector {
  display: inline-block;
  text-align: center
}

За счёт строчно-блочности такой элемент получается ровно по размеру контента, и, что очень важно, text-align родителя ставит его на нужное место. А уже внутри элемента используем тот text-align, который нам нужен, чаще всего center.

Важно, что работу этого внутреннего text-align мы вообще не ощущаем до тех пор, пока во flex-элементе всего одна строка.

Кстати, inline-flex тоже подойдёт!

Примеры кода



Данная схема повышает универсальность решения, но нужна далеко не всегда. Например, меню сложно представить сделанным в две строки, поэтому и обёртка нас не спасёт – надо превращать меню в кнопку.

Дополнительные мысли


Отступы


Сейчас элементы перед тем, как соскочить на отдельные строки, подходят вплотную друг к другу. Как же добавить между ними отступ, не отодвинув их от краёв контейнера?
Эту задачу легко решает классический приём: padding — элементам и отрицательный margin – родителю. Готовый пример.

Зеркальность


Разумеется, любой из наших примеров можно отзеркалить за счёт wrap-reverse, flex-direction, margin-right: auto и т.п. Например, меню выше логотипа смотрится хорошо.

Заключение


Данный приём является просто интересным решением определённой задачи. Разумеется, не надо отказываться от media-запросов и верить во flexbox без media. Но в рассмотренных примерах, на мой взгляд, данное решение представляется очень интересным.

Также есть подробный видеоразбор данного приёма. Однако, я не был уверен, что в первой статье на хабре стоит давать ссылку на youtube-канал, поэтому постарался добавить в пост побольше картинок, в том числе gif.

Надеюсь, рассмотренная схема работы с flex-элементами окажется полезной и облегчит жизнь верстальщиков при решении определённых задач!

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 18

    +2

    Да, помню, как пробовал решить Вашу задачу, будучи уверенным, что это не так сложно, но на втором часу понял, что тут есть какой-то нетривиальный приём, который не сильно известен, но работает.


    Решение однозначно не является тривиальным и это решение однозначно не то, что первым придёт в голову.


    То, что такое решение есть вообще и то, что оно было найдено — очень круто.


    Спасибо за статью, Дмитрий.

      +2

      решение, конечно, интересное, но с эпитетами вышел небольной перебор :)


      озарение
      безумно
      чудеса
      трудно поверить
      фантастический факт
      тотальный парадокс
      очень интересным
        +1
        Спасибо, учту на будущее. Просто когда нашёлся flex-grow: 0.5 и в целом это решение, то эмоций было много. Вот в статье они тоже вылезли.
        +1

        Просто респект! Как раз на днях такая задача попалась, тоже вертел туда-сюда, в итоге забил и поставил медиа-запрос. Попробую переделать)

          +1
          Понравилась статья. Думаю, в определенных ситуациях это даже лучше, чем медиа-запросы. Спасибо, буду использовать.
            0
            jsfiddle.net/2p8x1dvf

            По-моему, в этом примере не хватает переноса элементов меню на следующую строку, для экранов меньше 465px. Для этого достаточно к .menuRow добавить 2 стиля:

            flex-wrap: wrap;
            justify-content: center;


            А вообще, статья очень классная!
              0
              Мне кажется, заказчик не согласится на меню, идущее в 2 строки. Надо где-то в районе 580px для телефонов его в кнопку превратить.

              То есть, на мой взгляд, в примере с меню мы не можем обойтись полностью без media-запросов. Но данный приём с flex позволяет значительно дольше не переживать по поводу того, что меню соскочит вниз.
              0

              Мне кажется, что в flex-grow играет роль не само значение, а отношение к другим. То есть если все будут по 1 или по 2, будет то же самое, разве нет?

                0

                Тоже самое будет ровно до момента переноса элемента на следующую строку. И вот там уже будет отличие между 1 и 0.5

                  0
                  На меленьких экранах любой flex-grow кроме 0.5 не позволит сделать центровку. Когда grow >= 1 элемент съедает всё пространство, и text-align разбросают по краям его контент. У верхнего дефолтный left, у нижнего right проставлен.
                    0

                    В том-то и дело, что нет:


                    У значений flex между 0 и 1 несколько особенное поведение: когда сумма значений flex в строке меньше 1, они занимают меньше чем 100% свободного пространства.
                    0

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

                      0
                      Не соглашусь что задача редкая. В статье описан пример с менюшкой, на мой взгляд, это как раз тот случай где можно и даже нужно использовать такой подход для перестраховки.
                      Этот подход не заменяет медиазапросы, но позволяет снять лишнюю головную боль…
                      0
                      тоже самое можно сделать с помощью flex-grow:9999:
                      jsfiddle.net/wh1oesac/1
                      joren.co/flex-grow-9999-hack
                        0
                        Это не работает. В песочнице отлично видно, что текст заголовока остаётся прижат к левому краю после переноса даты на новую строку.
                        0
                        очень интересное решения)
                          0

                          Мне статья очень понравилась как пример "outside the box thinking" и важности внимания к мелочам: почти неизвестная (я сам узнал о ней впервые) особенность стандарта, упомянутая лишь вскользь в примечании к спецификации, в умелых руках выручает во вполне реальной практической задаче. И в целом приятно видеть такое подробное и наглядное объяснение, как всё работает. Спасибо!

                            0
                            Очень интересное и полезное решение. Спасибо.

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

                            Самое читаемое