Однако, недавно у меня возникло стойкое ощущение, что нужно поделиться одним нетривиальным и, на мой взгляд, полезным приёмом, связанным с flexbox. Написать пост побудил тот факт, что ни один знакомый (из учеников, верстальщиков и просто людей, близких к web), не смог решить задачку, связанную с flexbox, хотя на это нужно всего 4-6 строк.
Итак, сначала постановка задачи, затем предыстория и, наконец, решение.
Постановка задачи
- На большом экране два элемента расположены горизонтально, при адаптивке на телефонах – вертикально.
- При этом на больших экранах они должны прижиматься к краям (как при 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-элементами окажется полезной и облегчит жизнь верстальщиков при решении определённых задач!
Комментарии (15)
CoolCmd
26.10.2019 12:37+2решение, конечно, интересное, но с эпитетами вышел небольной перебор :)
озарение
безумно
чудеса
трудно поверить
фантастический факт
тотальный парадокс
очень интереснымdmitry-lavrik Автор
26.10.2019 13:20+1Спасибо, учту на будущее. Просто когда нашёлся flex-grow: 0.5 и в целом это решение, то эмоций было много. Вот в статье они тоже вылезли.
diman2000linda
26.10.2019 14:34+1Просто респект! Как раз на днях такая задача попалась, тоже вертел туда-сюда, в итоге забил и поставил медиа-запрос. Попробую переделать)
DKozachenko
26.10.2019 16:50+1Понравилась статья. Думаю, в определенных ситуациях это даже лучше, чем медиа-запросы. Спасибо, буду использовать.
pavelgvozdb
27.10.2019 09:50jsfiddle.net/2p8x1dvf
По-моему, в этом примере не хватает переноса элементов меню на следующую строку, для экранов меньше 465px. Для этого достаточно к.menuRow
добавить 2 стиля:
flex-wrap: wrap; justify-content: center;
А вообще, статья очень классная!dmitry-lavrik Автор
27.10.2019 12:04Мне кажется, заказчик не согласится на меню, идущее в 2 строки. Надо где-то в районе 580px для телефонов его в кнопку превратить.
То есть, на мой взгляд, в примере с меню мы не можем обойтись полностью без media-запросов. Но данный приём с flex позволяет значительно дольше не переживать по поводу того, что меню соскочит вниз.
da411d
27.10.2019 11:19Мне кажется, что в flex-grow играет роль не само значение, а отношение к другим. То есть если все будут по 1 или по 2, будет то же самое, разве нет?
Per_Ardua
27.10.2019 11:51Тоже самое будет ровно до момента переноса элемента на следующую строку. И вот там уже будет отличие между 1 и 0.5
dmitry-lavrik Автор
27.10.2019 12:07На меленьких экранах любой flex-grow кроме 0.5 не позволит сделать центровку. Когда grow >= 1 элемент съедает всё пространство, и text-align разбросают по краям его контент. У верхнего дефолтный left, у нижнего right проставлен.
profesor08
28.10.2019 08:00Забавный случай, но задача очень редкая, а результат немного хаотичен. В подавляющем большинстве случаев, хотят чтоб выглядело все одинаково, несмотря на то, что подобные решения позволяют запихнуть максимум контента в отведенное пространство.
alexmixaylov
29.10.2019 15:53Не соглашусь что задача редкая. В статье описан пример с менюшкой, на мой взгляд, это как раз тот случай где можно и даже нужно использовать такой подход для перестраховки.
Этот подход не заменяет медиазапросы, но позволяет снять лишнюю головную боль…
Xo4yHaMope
29.10.2019 15:54тоже самое можно сделать с помощью flex-grow:9999:
jsfiddle.net/wh1oesac/1
joren.co/flex-grow-9999-hackdmitry-lavrik Автор
29.10.2019 15:55Это не работает. В песочнице отлично видно, что текст заголовока остаётся прижат к левому краю после переноса даты на новую строку.
Zeben
Да, помню, как пробовал решить Вашу задачу, будучи уверенным, что это не так сложно, но на втором часу понял, что тут есть какой-то нетривиальный приём, который не сильно известен, но работает.
Решение однозначно не является тривиальным и это решение однозначно не то, что первым придёт в голову.
То, что такое решение есть вообще и то, что оно было найдено — очень круто.
Спасибо за статью, Дмитрий.