Перейти к содержимому

Flexbox в деталях: оси, grow/shrink/basis, паттерны

Flexbox — модуль раскладки для одномерного потока: строка или колонка элементов, которые должны гибко делить между собой свободное место. Если каскад из предыдущей главы — это «правила игры», то flexbox — первый «спортивный инвентарь»: с ним ты собираешь навигации, карточки, тулбары и формы. В краткой версии ты применял flex и justify-center наугад. Здесь разбираем механику: как считаются размеры айтемов, почему контент «не сжимается» и почему одно свойство ломает всю раскладку.

В продакшене flex — самый частый инструмент раскладки компонентного уровня: кнопка с иконкой, карточка с футером, шапка с лого. А вместе с ним — самые частые баги: переполнение текстом, «лишний» скролл, айтемы разной высоты. Всё это решается пониманием трёх свойств flex-grow, flex-shrink, flex-basis и одного дефолта min-width: auto.

Flex-контейнер живёт в системе координат из двух осей. flex-direction задаёт главную ось, вторая — поперечная:

flex-direction: row (дефолт) flex-direction: column
main axis ──────────────────▶ │
┌────┐ ┌────┐ ┌────┐ ▼ main axis
│ A │ │ B │ │ C │ ┌────┐
└────┘ └────┘ └────┘ │ A │
▲ ▲ ▲ └────┘
│ │ │ ┌────┐
cross axis (вертикаль) │ B │
└────┘
┌────┐
│ C │
└────┘
◀────┘ cross axis (горизонталь)

Правило-мнемоника: justify-* всегда вдоль main, align-* всегда вдоль cross — независимо от направления. justify-content в column-режиме выравнивает по вертикали. Запомнил это — половина путаницы с flex ушла. Все значения всех flex-свойств с наглядными графиками собраны в классической шпаргалке CSS Tricks: A Complete Guide to Flexbox — держи её открытой, пока идёшь по этой главе.

.nav {
display: flex; /* или inline-flex — контейнер в строке */
flex-direction: row; /* row | row-reverse | column | column-reverse */
flex-wrap: nowrap; /* nowrap | wrap | wrap-reverse */
gap: 1rem; /* отступы МЕЖДУ айтемами: row-gap + column-gap */
}

gap — важное новшество эпохи flex (раньше мучились с margin’ами и отрицательными margin’ами на контейнере). Пробелы появляются только между айтемами, по краям — чисто.

flex-flow — шорткат direction + wrap: flex-flow: row wrap.

justify-content:
flex-start (дефолт) center flex-end
┌────┐┌────┐┌────┐ ┌────┐┌────┐┌────┐ ┌────┐┌────┐┌────┐
└────┘└────┘└────┘ └────┘└────┘└────┘ └────┘└────┘└────┘
space-between space-around space-evenly
┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐
└────┘ └────┘ └────┘ └────┘ └────┘ └────┘ └────┘ └────┘ └────┘
(края вплотную) (полу-пустоты по краям) (равные пустоты везде)

align-items: выравнивание вдоль cross (одна строка)

Заголовок раздела «align-items: выравнивание вдоль cross (одна строка)»
align-items:
stretch (дефолт) flex-start center
┌────┐ ┌──────────┐ ┌──┐ ┌────┐ ┌────┐ ┌──┐ ┌────┐ ┌────┐ ┌──┐
│ │ │ │ │ │ └────┘ └────┘ └──┘ │ │ │ │ │ │
└────┘ └──────────┘ └──┘ └────┘ └────┘ └──┘
(вытянуты по высоте самого высокого)
baseline flex-end
┌────┐ ┌──────────┐ ┌────┐
│ │ │ │ │ │ ┌────┐
└────┘ └──────────┘ └────┘ │ │
(по базовой линии текста) └────┘ └──┘

align-content: распределение СТРОК вдоль cross (при flex-wrap)

Заголовок раздела «align-content: распределение СТРОК вдоль cross (при flex-wrap)»

Работает только когда айтемов больше одной строки (flex-wrap: wrap и реальное перенасыщение). Принимает те же значения, что и justify-content (stretch, flex-start, center, space-between, …). Если строка одна — не влияет ничего, и это классическая грабля: «почему align-content: center не центрирует мой одинокий айтем?» — потому что для одной строки есть align-items.

Каждый flex-айтем имеет три крана, регулирующих его размер:

.item {
flex-grow: 0; /* могу ли я РАСТИ, если есть свободное место? (коэффициент) */
flex-shrink: 1; /* могу ли я СЖИМАТЬСЯ, если места не хватает? (коэффициент) */
flex-basis: auto; /* мой стартовый размер до распределения (как width по main) */
}

Сценарий с grow. Контейнер 800px, три айтема flex-basis: 200px каждый → сумма 600px, свободных 200px. Если у всех flex-grow: 1, свободное место делится поровну: каждый получает 200 + 66.67 = 266.67px. Если первому flex-grow: 2, остальным 1: свободные 200px делятся на 4 доли (2+1+1), первый забирает 100px → 300px, остальные по 50px → 250px.

flex-grow: 2, 1, 1 при контейнере 800 и basis 200:
свободных 200px → 4 доли → 1 доля = 50px
┌────────── 300 ──────────┐┌──── 250 ────┐┌──── 250 ────┐
│ flex-grow: 2 ││ grow: 1 ││ grow: 1 │
└─────────────────────────┘└─────────────┘└─────────────┘

Сценарий со shrink. Контейнер 500px, три айтема по flex-basis: 200px (сумма 600, не хватает 100). Все с flex-shrink: 1: сжатие распределяется пропорционально basis: каждый теряет 100 × (200/600) = 33.3 → по 166.67px. Формула общая:

вклад айтема в сжатие = недостача × (basis × shrink) / Σ(basis × shrink)

Важно: shrink работает относительно basis, а не текущего размера. flex: 1 на узком айтеме сожмёт его меньше, чем flex: 1 на широком, — пропорция сохраняется. Браузерные алгоритмы этого перераспределения шаг за шагом разобраны в MDN: Контролирование соотношения элементов по главной оси — читать сразу после этой главы.

flex: 1; /* = 1 1 0% — расти, сжиматься, стартовать с нуля.
Все одинаковые flex:1 поделят место ПОРОВНУ */
flex: auto; /* = 1 1 auto — расти/сжиматься, стартовать от контента */
flex: initial; /* = 0 1 auto — дефолт: не расти, сжиматься, от контента */
flex: none; /* = 0 0 auto — жёсткий бокс: не расти и не сжиматься */

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

.item--tall { align-self: stretch } /* переопределение align-items для одного айтема */
.item--first { order: -1 } /* визуальный порядок: по умолчанию 0, меньше — раньше */

order меняет только ВИЗУАЛЬНЫЙ порядок: для screen reader’ов, фокуса по Tab и поисковиков айтем остаётся на месте в DOM. Выносить кнопку «Купить» первой через order — ломать доступность.

Дефолт у flex-айтема: min-width: automin-height: auto для column-оси). То есть айтем не может стать уже своего контента. Длинное слово или непрерывная строка растянет flex-строку, родитель получит горизонтальный скролл, и ни flex-shrink, ни overflow внутри сами по себе не помогут.

.item {
min-width: 0; /* разрешаем сжиматься ниже контента */
}
.item .text {
overflow: hidden;
text-overflow: ellipsis; /* и красиво обрезаем */
white-space: nowrap;
}
ДО min-width: 0: ПОСЛЕ:
контейнер ──────────────────────▶ контейнер ────────────────▶
┌────────────┐┌──────────────────────────────────┐ ┌───────┐┌──────────────────┐
│ sidebar ││ вот-очень-длинная-строка-которая │ │sidebar││ вот-очень-длинна…│
│ 200px ││ не-сворачивается-и-тянет-скролл │ │ ││ │
└────────────┘└──────────────────────────────────┘ └───────┘└──────────────────┘
▲ скролл на весь контейнер контент обрезан

То же самое в column-режиме с высотой: min-height: 0 на flex-айтеме — обязательное условие, чтобы внутренний overflow: auto заработал и дал скролл внутри, а не раздул всю страницу.

<div class="hero">
<h1>Привет</h1>
</div>
.hero {
display: flex;
justify-content: center; /* по main */
align-items: center; /* по cross */
min-height: 100dvh;
}

До flex это были position: absolute + transform или таблицы. Сейчас — две строки.

<div class="cards">
<article class="card">
<h3>Заголовок</h3>
<p>Текст разной длины…</p>
<a class="card__link" href="#">Читать</a>
</article>
<!-- ещё карточки -->
</div>
.cards {
display: flex;
gap: 1rem;
align-items: stretch; /* дефолт: все растянутся до высоты ряда */
}
.card {
flex: 1; /* равные ширины */
display: flex;
flex-direction: column; /* внутри — вертикальный флекс */
}
.card__link {
margin-top: auto; /* прижимаем футер карточки к низу */
}

Приём «колонка внутри строки»: внешний flex делает равную высоту, внутренний column-флекс с margin-top: auto у футера — разводит контент. Это заменяет абсолютное позиционирование и грид-хаки для карточек.

.layout {
display: flex;
gap: 2rem;
align-items: flex-start; /* колонки не растягиваем по высоте друг друга */
}
.sidebar {
flex: 0 0 260px; /* жёсткая колонка: не расти, не жаться, 260px */
/* эквивалент: width: 260px; flex-shrink: 0; */
}
.main {
flex: 1; /* занимает всё остальное */
min-width: 0; /* и даём своему контенту сжиматься! */
}

Шапка: лого слева, меню по центру, пользователь справа

Заголовок раздела «Шапка: лого слева, меню по центру, пользователь справа»
.header {
display: flex;
align-items: center;
gap: 1rem;
}
.header__menu {
margin-inline: auto; /* авто-маргины поглощают свободное место */
}

Классика: margin-left: auto на последнем элементе прижимает его вправо, margin-inline: auto на среднем центрирует. Никаких float и абсолютов.

Заголовок раздела «Sticky footer: прижать футер к низу при малом контенте»
<body class="page">
<header>…</header>
<main class="page__main">…</main>
<footer>…</footer>
</body>
.page {
display: flex;
flex-direction: column;
min-height: 100dvh;
}
.page__main {
flex: 1; /* растягивается, выталкивая футер вниз */
}

При переполнении страница просто растёт — скролл работает естественно. Старые хаки с min-height: 100% на html/body больше не нужны.

  1. Горизонтальный скролл из-за длинного контента в flex-строке — грабля min-width: auto. Лечится min-width: 0 на айтеме + overflow на контенте.
  2. flex: 1, а айтемы разной ширины. Контент шире базы съедает «равенство». Лечится тем же min-width: 0 или явным flex-basis: 0 + обрезкой контента.
  3. Пробелы-«буквы» между inline-block элементами — наследие старого подхода; при display: flex пробелы в разметке не создают отступов, используй flex.
  4. Центрирование через justify-content в column-режиме «не работает». Помни мнемонику: justify — всегда вдоль main; в column main — вертикаль. Хочешь по горизонтали в column — это align-items.
  5. align-content на одной строке. Работает только при переносе на несколько строк; одну строку центрирует align-items.
  6. Менять визуальный порядок через order, забывая про Tab и скринридер. Визуальный порядок ≠ порядок фокуса. Для доступности порядок правят в DOM.
  7. height: 100% на flex-айтеме «не работает». В flex-строке высота определяется align-items/stretch; используй align-self: stretch (дефолт) и задавай минимальную высоту контейнеру, а не проценты айтему.
  8. Делать gap отрицательным «чтобы карточки встали по краям». Для этого есть раскладка (отрицательные margin на обёртке для компенсации) или, правильнее, grid с auto-fill.
  1. Чем отличается justify-content от align-items? Работают по разным осям: justify — вдоль главной (main), align — вдоль поперечной (cross). При flex-direction: column их роли меняются визуально, но не семантически.
  2. Что делает flex: 1 и чем оно отличается от flex-grow: 1? flex: 1 = flex-grow: 1; flex-shrink: 1; flex-basis: 0% — старт от нуля, поровну. flex-grow: 1 при прочих дефолтах (shrink: 1, basis: auto) стартует от контента: размеры будут равны только если контент одинаков.
  3. Как браузер распределяет свободное место при разных flex-grow? Свободное место делится пропорционально коэффициентам grow. Айтем с grow 2 получит в два раза больше добавочных пикселей, чем с grow 1: итог = basis + свободное × (grow / Σgrow).
  4. Почему flex-элемент не сжимается меньше контента и как это исправить? Дефолт min-width: auto запрещает. min-width: 0 снимает ограничение; обычно в паре с overflow: hidden и text-overflow: ellipsis на текстовом контенте.
  5. Что такое align-content и когда он работает? Распределение flex-СТРОК вдоль cross-оси. Активен только при flex-wrap: wrap и наличии двух и более строк; с одной строкой не влияет.
  6. Как прижать футер к низу страницы независимо от контента? body — column-флекс на min-height: 100dvh, main — flex: 1. При коротком контенте растягивается и выталкивает футер, при длинном страница растёт естественно.
  7. order влияет ли на доступность / порядок табуляции? Нет: меняет только визуальный порядок. DOM, фокус по Tab и порядок для assistive-технологий остаются прежними — поэтому менять порядок важных интерактивных элементов через order нельзя.
  8. Когда flex, а когда grid? Flex — одномерное расположение (ряд или колонка компонентов с гибким распределением места). Grid — двумерные сетки (колонки И строки одновременно). Компоненты внутри — flex, макет страницы и карточные сетки — grid.
  1. Диагностика. Сверстай flex-строку из сайдбара 260px и контента. Положи в контент длинную непрерывную строку (без пробелов), добейся горизонтального скролла, затем почини через min-width: 0. Критерий: можешь объяснить, почему помогло именно это свойство.
  2. Формула grow. Три айтема flex-basis: 150px в контейнере 900px с коэффициентами flex-grow: 1, 2, 3. Посчитай итоговые ширины на бумаге, сверь с DevTools → Computed → flex-basis/flex-grow. Критерий: совпадение до десятых пикселя.
  3. Карточки. Сделай 3 карточки в ряд одинаковой высоты с кнопкой, прижатой к низу каждой карточки, используя вложенный column-флекс и margin-top: auto. Критерий: при разном объёме текста кнопки стоят на одной линии.
  4. Шапка. Собери шапку: лого слева, навигация по центру, аватар справа — только flex-свойства, без absolute и float. Критерий: центр навигации остаётся по центру при разной ширине лого и аватара.
  5. Sticky footer. Собери страницу header/main/footer, где футер всегда внизу при любом объёме контента (проверь и пустой main, и main на 5 экранов). Критерий: нет ни зазора под футером, ни наложений.

Если синтаксис ещё не сидит в пальцах — пройди Flexbox Froggy: 24 уровня, каждый закрепляет ровно одно свойство из этой главы. Первоисточник по терминологии — спецификация CSS Flexible Box Layout Module.