Flexbox вглубь: grow, shrink, basis

Разбираем три числа за свойством flex и понимаем, как элементы делят место, растут и сжимаются.

flex — это шорткат из трёх значений: flex-grow (как расти), flex-shrink (как сжиматься) и flex-basis (стартовый размер до распределения).

Зачем это знать на практике

Большинство «непонятных» багов Flexbox — это непонимание трёх чисел внутри flex. Почему один блок не сжимается? Почему flex: 1 игнорирует заданную ширину? Почему текст вылезает за контейнер, хотя стоит overflow: hidden? Все ответы — в механике grow/shrink/basis и в коварном дефолтном min-width: auto. Разобравшись, вы перестанете подбирать значения наугад и будете писать раскладку, которая ведёт себя предсказуемо при любом контенте.

Три кита: grow, shrink, basis

Когда контейнер становится display: flex, каждый дочерний элемент получает три параметра:

СвойствоДефолтЧто делает
flex-grow0множитель, как делить лишнее свободное место
flex-shrink1множитель, как отдавать место при нехватке
flex-basisautoстартовый размер по главной оси до раздачи/сжатия

Сначала браузер расставляет элементы по их flex-basis. Затем смотрит: место осталось — раздаёт его пропорционально flex-grow; места не хватает — забирает пропорционально flex-shrink. Дефолт 0 1 auto означает «не расти, но при тесноте — сжиматься, стартуя от своего естественного размера».

Шорткат flex и его пресеты

Писать три свойства руками не нужно — есть flex. Важно знать его типовые формы, потому что одно число разворачивается не очевидно:

.item { flex: 1; }        /* = 1 1 0   — равные колонки от нулевого базиса   */
.item { flex: auto; }     /* = 1 1 auto — растут и сжимаются от своего размера */
.item { flex: none; }     /* = 0 0 auto — жёсткий, не растёт и не сжимается   */
.item { flex: 0 1 200px; }/* стартует с 200px, сжимается, но не растёт        */

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

/* три равные колонки независимо от длины текста */
.row { display: flex; gap: 16px; }
.row > * { flex: 1; }   /* 1 1 0 — равная ширина */

flex-basis против width

По главной оси flex-basis побеждает width: если задан и тот, и другой, базисом считается flex-basis (кроме flex-basis: auto, который как раз и означает «возьми width или размер контента»). Поэтому flex: 1 (с basis 0) делает заданную width бессмысленной — это частый сюрприз.

align-self: один элемент против всех

align-items на контейнере задаёт выравнивание по поперечной оси для всех; align-self на конкретном элементе переопределяет его лично. Удобно, например, прижать одну иконку к низу, когда остальные центрированы:

.toolbar {
  display: flex;
  align-items: center;   /* все по центру */
  gap: 12px;
}
.toolbar .logo { align-self: flex-start; } /* логотип — к верхнему краю */

Перенос строк: flex-wrap и gap

По умолчанию flex-элементы стоят в одну строку и сжимаются как угодно, лишь бы поместиться (flex-wrap: nowrap). Чтобы они переносились, нужен flex-wrap: wrap. В сочетании с flex-basis и flex-grow это даёт «резиновую» сетку:

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;            /* зазор и по строкам, и по столбцам */
}
.tags > * {
  flex: 1 1 160px;     /* не уже 160px, дальше растут и переносятся */
}

Свойство gap в Flexbox задаёт зазоры сразу по обеим осям и давно поддерживается во всех актуальных браузерах — это лучше старых «костылей» с отрицательными margin. При flex-wrap: wrap распределением строк по поперечной оси управляет align-content (не путать с align-items, который выравнивает элементы внутри строки).

Как это работает под капотом

Раскладка flex-строки — это решение задачи о делении длины. Браузер берёт «гипотетический» размер каждого элемента (его flex-basis, разрешённый из width/контента) и считает суммарную свободную длину контейнера. Если свободная длина положительна, она раздаётся: каждый элемент получает прибавку, пропорциональную своему flex-grow относительно суммы всех grow. Если отрицательна (элементы не влезают), сжатие распределяется пропорционально произведению flex-shrink × flex-basis — то есть крупные элементы с ненулевым shrink отдают больше. Затем к каждому элементу применяются ограничения min-width/max-widthmin-height/max-height на колоночной оси). И вот тут вступает главный подводный камень: у flex-элементов min-width по умолчанию равен auto, а не 0. Это значит, что элемент не сожмётся уже своего минимального контента — длинного слова, картинки, pre — даже если flex-shrink положителен. Именно поэтому контент иногда «прорывает» контейнер.

Частые ошибки

min-width: auto ломает сжатие и переполняет контейнер. Самая частая боль Flexbox: длинный текст или <pre> внутри flex-элемента не даёт ему сжаться, появляется прокрутка. Решение — явно разрешить сжатие:

.flex-item {
  min-width: 0;   /* разрешаем сжаться уже контента */
  overflow: hidden;
}

Думают, что flex: 1 уважает width. flex: 1 = 1 1 0, базис обнулён, поэтому заданная width игнорируется. Нужна стартовая ширина — пишите flex: 1 1 240px или flex: 0 1 240px.

Путают align-items и align-content. При flex-wrap: wrap за расположение целых строк отвечает align-content; align-items выравнивает элементы внутри одной строки. На однострочном контейнере align-content вообще ни на что не влияет.

Забывают про flex-wrap. Без flex-wrap: wrap элементы не переносятся, а бесконечно сжимаются — и упираются как раз в min-width: auto.

Итоги

  • flex: grow shrink basis — три числа: как расти, как сжиматься, от какого размера стартовать.
  • flex: 1 = 1 1 0 (равные колонки, width игнорируется); flex: auto = 1 1 auto (учитывает контент); flex: none = 0 0 auto (жёсткий).
  • По главной оси flex-basis важнее width; align-self переопределяет align-items для одного элемента.
  • flex-wrap: wrap включает перенос; gap задаёт зазоры по обеим осям без отрицательных margin.
  • Дефолтный min-width: auto мешает сжатию и переполняет контейнер — лечится min-width: 0.
Проверьте себя
1. Во что разворачивается шорткат flex: 1?
Aflex-grow: 1; flex-shrink: 1; flex-basis: 0
Bflex-grow: 1; flex-shrink: 0; flex-basis: auto
Cflex-grow: 0; flex-shrink: 1; flex-basis: 100%
Dflex-grow: 1; flex-shrink: 1; flex-basis: auto
2. Почему длинное неразрывное слово в flex-элементе вылезает за контейнер, несмотря на flex-shrink: 1, и как это исправить?
AУ flex-элементов min-width по умолчанию равен auto; нужно задать min-width: 0
Bflex-shrink не работает с текстом; нужно убрать его
CКонтейнеру не хватает display: flex
DНужно увеличить flex-grow до 2
3. При flex-wrap: wrap какое свойство управляет расположением целых строк по поперечной оси?
Aalign-content
Balign-items
Cjustify-content
Dalign-self