Flex: делим свободное место своими руками

стили≈ 11 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

У трёх элементов ряда рост 1, 1 и 2. Третий вдвое шире соседей? Бывает, что да, а бывает, что он уже второго, — и flex тут не капризничает. Он не рисует пропорции на глаз, а считает: берёт базовые размеры, находит свободное место и раздаёт его по долям. Посчитаем раздачу руками, до запуска, и сверим с ответом. Заодно найдём умолчание, из-за которого ряды вылезают за край экрана, и свойство, которое меняет картинку, но не порядок элементов в разметке.

Разделы темы:·

До сих пор блоки вставали так, как велит нормальный поток: каждый следующий — под предыдущим. Свойство display: flex меняет правило для детей элемента. Сам элемент становится flex-контейнером, его прямые дети — flex-элементами, и встают они в ряд, один за другим. Flexbox есть во всех основных браузерах давно: Baseline widely с марта 2018 года.

Направление ряда называют главной осью. По умолчанию она горизонтальна, а flex-direction: column ставит её вертикально. В примере два контейнера по три коробки шириной 50px и высотой 20px: первый — ряд, второй — колонка под ним. Прогноз: какие x окажутся у коробок ряда и какие y — у коробок колонки?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Ряд поставил коробки вплотную: 0, 50, 100. Колонка похожа на обычный поток: первая коробка стоит на 20px, сразу под рядом высотой 20px, дальше — через каждые 20px. Похожа, но это не поток, и разница видна на отступах — во врезке ниже.

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

В нормальном потоке вертикальные отступы двух соседей схлопываются в один, больший. Колонка flex тоже ставит блоки друг под другом, и соблазнительно ждать того же. Две пары коробок высотой 40px с отступами по 20px сверху и снизу: первая пара — в обычном блоке .flow, вторая — в колонке .col. Прогноз: на каких y встанут коробки каждой пары?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
В потоке вторая коробка стоит на 80px: 20 отступа, 40 коробки и один промежуток 20 — нижний отступ первой и верхний второй слились. В колонке между коробками 240 − 160 − 40 = 40px: отступы сложились. Нижний отступ второй коробки .flow вытек из своего блока, так что колонка начинается на 140px. А её первая коробка стоит на 160px: из flex-контейнера отступ ребёнка не вытекает.

Это не недосмотр браузеров. Схлопывание — правило нормального потока, а спецификация CSS Flexbox прямо записывает: отступы соседних flex-элементов не схлопываются. Раз отступы здесь складываются, промежутки во flex задают не ими, а свойством контейнера gap — о нём в следующем разделе.

У каждого flex-элемента три значения, и задают их обычно одним свойством: flex: 1 1 40px — это flex-grow, flex-shrink и flex-basis по порядку. Рост, сжатие и база. Ряд браузер раскладывает как задачку в три шага, и если решать её в том же порядке, угадывать ширину больше не придётся.

База. flex-basis говорит, какой ширины элемент был бы сам по себе.
Свободное место. Из ширины контейнера вычитаются все базы и промежутки.
Доли. Если остаток положительный, он делится пропорционально flex-grow, и доля каждого прибавляется к его базе.

flex-grow делит свободное место, а не ширину. Рост 2 значит «вдвое больше прибавки», а не «вдвое шире соседа».

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

ЭлементБаза, flex-basisРост, flex-growИтоговая ширина
.a40px1?
.b80px1?
.c40px2?

Прогноз — три ширины через запятую, в порядке разметки. Разбор под примером назовёт числа: сверьте их со своим расчётом, а не просто прочитайте.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Базы заняли 40 + 80 + 40 = 160px, свободно 240 − 160 = 80px. Долей роста четыре (1 + 1 + 2), одна доля — 20px. .a получает одну долю и становится 60px, .b тоже одну — 100px, .c — две, 80px. Рост у .c вдвое больше, чем у .b, а сама она уже: вдвое больше ей досталось прибавки, а не ширины.

Отсюда понятно и привычное flex: 1 у всех элементов ряда. Короткая запись flex: 1 означает рост 1, сжатие 1 и базу 0%. Базы нулевые, всё место свободно и делится поровну — колонки выходят равными. Равенство ломают две вещи: внутренний отступ padding (база не бывает меньше него) и содержимое шире доли — о нём раздел про min-width ниже.

Если расти некому — у всех flex-grow 0, как по умолчанию, — свободное место остаётся пустым. Куда его сдвинуть, решает свойство контейнера justify-content: к началу ряда (так по умолчанию), к концу, равномерно между элементами или по краям с промежутками вдвое меньше. Это свойство отвечает за «прижать кнопку к правому краю шапки» — один из самых частых приёмов в вёрстке.

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

Промежутки между элементами задаёт gap на контейнере: gap: 10px ставит 10px между соседями, но не по краям ряда. Вычитаются промежутки на втором шаге, вместе с базами, — посчитаете это в первой задаче. gap во flex — Baseline widely с 26 октября 2023 года: он давно есть во всех основных браузерах, пишите смело.

До flex ряды собирали из элементов с display: inline-block. Такой элемент стоит в строке, как слово, но принимает ширину и высоту. И, как слово, он отделён от соседа пробелом — тем самым переносом строки, что стоит в разметке между тегами. Три таких элемента по 80px в контейнере 240px, каждый на своей строке разметки, и те же три во flex-ряду с gap: 0. Прогноз: одинаковые ли y у тройки в каждом контейнере?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
В первом контейнере 80 + 80 + 80 — ровно 240, но между элементами стоят два пробела: третий не помещается и уходит на следующую строку. Ширину пробела прибор здесь нарочно не мерит: она зависит от шрифта. Во flex-ряду пробелы между тегами не превращаются ни в элементы, ни в щели, и промежуток ровно такой, какой задан в gap.

Щель лечили кто как мог: вырезали переносы строк между тегами, подтягивали элементы отрицательным margin. Когда ряды переехали на flex, промежутки ещё долго делали отступами у элементов, а лишний крайний отступ гасили отрицательным margin у контейнера. gap во flex первым получил Firefox в 2018 году, Chrome — в 2020-м, а Safari — только в версии 14.1, 26 апреля 2021 года. Отсюда и дата в основном тексте: Baseline widely наступает через 30 месяцев после того, как возможность появилась во всех основных браузерах.

Решать такие задачки на живой странице помогают инструменты разработчика. В Chrome во вкладке Elements рядом с flex-контейнером стоит значок flex: щелчок по нему подсвечивает поверх страницы контейнер, его элементы и промежутки. Firefox во вкладке Inspector («Инспектор» в русской версии), на панели Layout («Разметка»), выписывает для выбранного flex-элемента, как получился его размер: базовый размер, насколько элемент вырос или сжался, минимум, если он сработал, и конечный размер. Как пользоваться самой вкладкой, разобрано в статье про вкладку Elements.

Свободное место бывает и отрицательным: базы в сумме шире контейнера. Тогда третий шаг идёт наоборот — браузер не раздаёт, а отбирает, и распоряжается этим второе число, flex-shrink. Его умолчание — 1: сжиматься согласны все.

В ряду 200px три элемента шириной 100, 200 и 100px. flex: 0 1 auto — это умолчание, записанное явно: не расти, сжиматься, а база auto берётся из width. Базы в сумме 400px, недостача — 200px. Прогноз: у всех отнимут поровну или как-то иначе, и какими окажутся три ширины?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Поровну вышло бы по 66,67px с каждого, и крайние элементы сжались бы до 33,33px. Браузер снял 50, 100 и 50: доля в недостаче — это flex-shrink, умноженный на базу. Сжатие у всех 1, базы относятся как 100 : 200 : 100, значит, и недостача делится как 1 : 2 : 1. Смысл простой: иначе элемент в 20px, у которого отнимают столько же пикселей, сколько у соседа в 200px, исчез бы первым.

Рост делится по одним долям, без оглядки на базу. Сжатие взвешивается по базе: широкий теряет больше пикселей, узкий — меньше.

Совсем не сжиматься элемент просит flex-shrink: 0 — так делают с иконками и аватарками, которым нельзя стать уже. В React Native, на котором пишут мобильные приложения, это умолчание: flexShrink там 0, и тесный ряд переполняется, а не сжимается. Это и ещё три расхождения с вебом разобраны в статье про flex с другими умолчаниями в React Native.

flex: 1 у обоих элементов обещает равные доли. Проверим обещание на ряду 200px, где в первом элементе лежит полоса шириной 150px. Под ним второй ряд, такой же, только первый элемент получил min-width: 0. Прогноз: четыре ширины подряд — два элемента первого ряда, потом два элемента второго.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Первый ряд разделился не поровну: 150 и 50. Виновато умолчание min-width: auto. У обычного блока оно значит ноль, а у flex-элемента — «не уже своего содержимого». Сжаться до доли в 100px первый элемент не может, пока внутри полоса в 150px, и недостающие 50px забирает у соседа. min-width: 0 снимает этот запрет: доли снова по 100px, а полоса выходит за границу своего элемента и прячется под соседа — на сцене её хвоста не видно.

В жизни такая полоса — длинное слово, адрес ссылки, таблица или блок кода. Карточка с flex: 1 не сжимается, ряд вылезает за край экрана, и ни рост, ни сжатие, ни база этого не исправят: элемент упёрся в свой минимум. Лечится одной строкой min-width: 0 у того элемента, который должен уступить. А что делать с содержимым, которое стало шире, решается уже внутри: перенести, обрезать или прокрутить. Прокрутку включает overflow: auto — всё, что не влезло в элемент, прокручивается внутри него.

Ряд вылез за край, хотя flex: 1 стоит у всех, — ищите внутри элемент шире своей доли и ставьте min-width: 0 тому, кто должен уступить.

Последнее свойство меняет не размеры, а место. order переставляет элемент в ряду: у всех по умолчанию 0, и чем меньше число, тем раньше элемент встаёт. В навигации ниже ссылку «Войти» хотят видеть первой и дали ей order: -1. Функция order() отвечает двумя строками: порядок в разметке и порядок на экране. Прогноз: совпадут ли они.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Экран послушался, разметка — нет. И это не мелочь: клавиша Tab ведёт фокус по разметке, программа чтения с экрана читает тоже по ней. Человек с клавиатурой видит «Войти» первой, а фокус попадает на неё третьей, после «Главной» и «Статей». То же с flex-direction: row-reverse: ряд развернулся, а разметка осталась прежней.

Отсюда правило: order и развороты годятся там, где порядок не несёт смысла, — например, картинка карточки слева или справа от текста. Если «Войти» по смыслу первая, переставьте её в разметке. Обход фокуса прибор проверить не может: настоящий Tab в песочнице не нажать. Проверьте руками — сохраните такой ряд в файл, откройте его в браузере и понажимайте Tab (в Safari по ссылкам ходит Option+Tab).

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

  • display: flex ставит прямых детей вдоль главной оси, flex-direction: column поворачивает её вертикально. Отступы flex-элементов не схлопываются: схлопывание — правило нормального потока.
  • Раздача идёт в три шага: база flex-basis, свободное место (контейнер минус базы и gap), доли по flex-grow, прибавленные к базам. Рост делит остаток, а не ширину; у flex: 1 база ноль, поэтому такие колонки делят ряд поровну.
  • Когда места не хватает, недостача снимается по flex-shrink, умноженному на базу: широкий теряет больше. flex-shrink: 0 запрещает сжиматься.
  • Умолчание min-width: auto не даёт flex-элементу стать уже содержимого: длинное слово или таблица ломают раздачу. Лечит min-width: 0 у того, кто должен уступить.
  • gap ставит промежутки только между соседями и вычитается до раздачи; во flex он Baseline widely с октября 2023 года.
  • order и row-reverse меняют картинку, а Tab и программы чтения идут по разметке. Раздачу на живой странице показывают инструменты разработчика: значок flex в Chrome и панель Layout («Разметка») в Firefox.

1/4Ряд шириной 300px, у первого элемента flex: 1 1 100px, у второго — flex: 2 1 50px. Какой ширины они станут?

2/4Ряд шириной 150px, в нём элементы шириной 200px и 100px, flex-shrink у обоих не задан. Какие ширины получатся?

3/4Ссылке «Войти» в конце навигации дали order: -1, и на экране она встала первой. Какой по счёту из трёх ссылок до неё дойдёт фокус при нажатии Tab?

4/4Две колонки с flex: 1, но в первой лежит таблица шире половины ряда, и колонки вышли неравными. Что выровняет доли?

Раздача с промежутками

Тот же ряд, что во втором разделе, но у контейнера появился gap: 10px. Посчитайте три ширины на бумаге: базы, промежутки, свободное место, доля. Потом запустите и сверьтесь.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Промежутков между тремя элементами два, по 10px, и вычитаются они вместе с базами: свободно 240 − (40 + 80 + 40) − 2 × 10 = 60px. Долей роста по-прежнему четыре, одна доля — 15px. Итог: 40 + 15 = 55px, 80 + 15 = 95px, 40 + 30 = 70px, то есть .row div → width: 55px, 95px, 70px.

Проверка: 55 + 95 + 70 = 220px и два промежутка по 10px дают те же 240px. По краям ряда gap места не занимает — только между соседями.

Боковая колонка сжалась

Раскладка: боковая колонка шириной 80px и основная с flex: 1, а в основной — таблица шириной 200px. Сначала предскажите, какие две ширины напечатает measure. Потом поправьте стили так, чтобы боковая колонка осталась 80px, основная заняла остаток ряда, а таблица прокручивалась внутри основной.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Вывод — .layout > div → width: 40px, 200px. Основная колонка не может стать уже таблицы: у неё умолчание min-width: auto, и её ширина застывает на 200px. Ряду не хватает 80 + 200 − 240 = 40px, а сжаться может только боковая колонка: у неё flex-shrink 1, и содержимого, которое бы её держало, нет. Она и отдаёт все 40px.

Первое, что хочется сделать, — flex-shrink: 0 у боковой колонки. Она останется 80px, но основная так и будет 200px, и ряд вылезет за контейнер на 40px. Лечить нужно причину — минимум основной колонки:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Теперь measure печатает 80px, 160px: база основной колонки ноль, ей достаются все свободные 160px, а таблица прокручивается внутри. Строго говоря, хватило бы одного overflow: auto: у элемента с прокруткой автоматического минимума нет. min-width: 0 оставляют, чтобы намерение было видно из стилей.

Войти справа без order

В навигации три ссылки по 70px в ряду 240px. Прижмите «Войти» к правому краю ряда одним новым правилом, не трогая разметку: без order и без justify-content у контейнера — «Главная» и «Статьи» должны остаться слева. Подсказка: свободное место можно отдать не только через flex-grow — у margin бывает значение auto, и во flex-ряду такой отступ забирает свободное место себе.

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Свободное место забирает автоматический отступ:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

measure печатает .bar a → x: 0px, 80px, 170px. Ссылки занимают 3 × 70 = 210px, промежутки — 20px, свободно 10px, и расти никто не хочет. Отступ auto во flex-ряду забирает всё свободное место: 10px встали слева от «Войти», в дополнение к промежутку. Она сдвинулась с 160 на 170px и упёрлась в край: 170 + 70 = 240.

Разметка не менялась, поэтому Tab идёт «Главная → Статьи → Войти» — в том же порядке, что на экране. С order картинка и порядок разошлись бы.