Тема 3. Layout: позиционирование, Flexbox, Grid, адаптивность
В теме 1 мы описали структуру документа, в теме 2 — оформление отдельных элементов: цвет, шрифт, размеры коробки. Но интерфейс — это не набор независимо оформленных блоков, а их взаимное расположение на плоскости: колонки рядом, шапка сверху, карточки сеткой, боковая панель, прижатая к краю. За это отвечает раскладка (англ. layout) — совокупность механизмов CSS, определяющих, где именно окажется каждый элемент. Раскладка — та часть CSS, которая исторически менялась сильнее всего: от табличной вёрстки девяностых через float к специально спроектированным Flexbox и Grid. Мы разберём её послойно — от потока документа, работающего по умолчанию, к позиционированию, затем к двум современным модулям раскладки, и завершим адаптивностью: тем, как одна вёрстка обслуживает экраны от телефона до широкого монитора.
Поток документа и модели отображения
Нормальный поток: блочные и строчные контексты
Пока к элементам не применены никакие свойства раскладки, браузер размещает их в нормальном потоке (англ. normal flow) — порядке по умолчанию, заданном спецификацией 1. Правило потока простое и предсказуемое: элементы идут в том же порядке, в каком записаны в разметке, и каждый следующий встаёт после предыдущего. Различие в том, как именно встаёт, — и здесь работают два контекста.
Блочные элементы (<div>, <p>, <section>, заголовки) выстраиваются сверху вниз, каждый с новой строки, и по умолчанию занимают всю доступную ширину родительского элемента — сколько бы места ни требовало содержимое. Два абзаца подряд окажутся один под другим, даже если в каждом всего одно слово.
Строчные элементы (<span>, <a>, <em>, <strong>) выстраиваются слева направо внутри строки, как слова в тексте, и переносятся на следующую строку только когда заканчивается место. Их ширина и высота определяются содержимым; задать им width или вертикальные отступы напрямую нельзя — это одно из типичных мест, где начинающий разработчик безуспешно пытается «раздвинуть» строчный элемент.
Нормальный поток — не архаизм, который нужно поскорее отключить, а разумная основа. Большая часть текстового контента прекрасно раскладывается именно им, и хорошая вёрстка обычно оставляет поток работать там, где он справляется, вмешиваясь только там, где нужна нестандартная геометрия.
Свойство display: block, inline, inline-block, none
К какому контексту относится элемент, определяет не только его тег, но и свойство display, которое можно переназначить 2. Четыре его базовых значения стоит знать наизусть.
display: block делает элемент блочным: новая строка, полная ширина, работают width, height, вертикальные отступы. display: inline — строчным: элемент течёт в строке, игнорирует заданные ширину и высоту. display: none полностью убирает элемент из потока и из отрисовки — он не занимает места и недоступен, как будто его нет в разметке (в отличие от visibility: hidden, который прячет элемент, но оставляет занятое им место).
Отдельного внимания заслуживает display: inline-block — гибрид, который течёт в строке рядом с соседями, как строчный, но при этом принимает width, height и вертикальные отступы, как блочный. Долгое время inline-block был рабочей лошадкой для раскладки горизонтальных рядов — кнопок, пунктов меню, карточек, — пока эту роль не переняли Flexbox и Grid. Сегодня его основное применение — точечное: элемент, который должен вести себя как слово в тексте, но с заданными габаритами.
Значения display этим не исчерпываются: flex и grid, которым посвящены отдельные разделы темы, тоже задаются через это свойство. Формально display управляет сразу двумя вещами — тем, как элемент ведёт себя снаружи (блочный или строчный по отношению к соседям), и тем, какой контекст раскладки он создаёт внутри для своих потомков.
Контекст форматирования и сворачивание полей
Контейнер с блочными потомками создаёт вокруг них блочный контекст форматирования (англ. block formatting context, BFC) — изолированную область, внутри которой действуют правила блочного потока 1. Понятие звучит абстрактно, но за ним стоят вполне ощутимые эффекты, и один из них мы уже встречали.
В теме 2, разбирая блочную модель, мы отметили, что вертикальные margin-ы соседних блоков схлопываются (англ. margin collapsing): при margin-bottom: 20px у одного блока и margin-top: 30px у следующего промежуток окажется 30 px, а не 50. Теперь можно назвать причину: схлопывание — свойство именно блочного контекста форматирования, в котором соприкасающиеся вертикальные поля объединяются в одно, равное большему из них. Схлопываются и поля вложенных элементов с полями родителя, если между ними нет рамки, padding-а или содержимого, — из-за чего margin-top дочернего элемента иногда «протекает» наружу и сдвигает весь родительский блок.
Новый BFC создаётся не всегда, а при определённых условиях: у элемента задан overflow отличный от visible, display: flow-root, он является flex- или grid-элементом, плавает через float. Внутри такого контекста поля больше не схлопываются с внешними, а плавающие элементы не выходят за его границы. display: flow-root — современное и наименее побочное средство завести чистый BFC, когда нужно, например, чтобы родитель охватывал вложенные плавающие элементы или чтобы прекратить нежелательное схлопывание.
Позиционирование
position: static, relative, absolute, fixed, sticky
Свойство position выводит элемент из-под управления нормального потока и позволяет задавать его положение явно, через смещения top, right, bottom, left 3. У него пять значений, и различаются они тем, относительно чего отсчитывается положение.
position: static — значение по умолчанию: элемент в нормальном потоке, смещения игнорируются. Это «выключенное» позиционирование.
position: relative оставляет элемент в потоке (место под него сохраняется), но позволяет сместить его визуально относительно исходного положения. Само по себе относительное позиционирование применяют нечасто; куда важнее его побочная роль — оно делает элемент точкой отсчёта для абсолютно позиционированных потомков.
position: absolute полностью извлекает элемент из потока — соседи смыкаются, как будто его нет, — и позиционирует его относительно ближайшего предка, у которого position отличен от static. Если такого предка нет, отсчёт идёт от корневого блока страницы. Отсюда устойчивый приём: родителю ставят position: relative, чтобы «поймать» абсолютно позиционированного потомка внутри своих границ. Так размещают бейджи на карточках, кнопки закрытия в углу модального окна, подсказки.
position: fixed тоже извлекает элемент из потока, но привязывает его к вьюпорту — видимой области окна. Такой элемент остаётся на месте при прокрутке страницы; типичные применения — «липкая» шапка, кнопка «наверх», плавающая панель действий.
position: sticky — гибрид относительного и фиксированного. Пока элемент в пределах видимости прокручивается обычным образом (как relative), но при достижении заданного порога (top: 0) «прилипает» к краю и держится там, пока родительский контейнер не уйдёт из виду. Это поведение заголовков разделов в длинных списках, которые остаются на виду, пока листаешь свой раздел, и уступают место следующему.
Контекст наложения и z-index
Как только элементы перестают идти строго друг за другом и начинают перекрываться, возникает вопрос: какой из них окажется сверху? Порядок наложения регулируется свойством z-index — целым числом, задающим положение элемента по воображаемой оси, направленной от экрана к зрителю. Больший z-index — ближе к зрителю, поверх остальных.
Тонкость в том, что z-index действует не глобально, а внутри контекста наложения (англ. stacking context) — изолированной группы, порядок внутри которой считается отдельно. Элемент с z-index: 9999 внутри одного контекста никогда не перекроет элемент из другого контекста, который стоит выше по дереву, — сколько бы девяток мы ни приписали. Новый контекст наложения создаётся не только через position с ненулевым z-index, но и рядом других свойств: opacity меньше единицы, transform, filter, will-change. Это частый источник недоумения: разработчик поднимает z-index до астрономических значений, а элемент всё равно оказывается под соседом — потому что проблема не в величине, а в том, что они в разных контекстах. Понимание границ контекста наложения устраняет большую часть подобных «магических» багов.
float как устаревающий механизм
Свойство float изначально решало узкую типографскую задачу — обтекание изображения текстом, как в бумажной вёрстке. Плавающий элемент сдвигается к краю контейнера, а последующий контент обтекает его сбоку. На безрыбье доспециализированных модулей раскладки float два десятилетия использовали не по прямому назначению — для построения многоколоночных сеток, выстраивания элементов в ряд, вёрстки целых страниц. Приём работал, но требовал костылей: очистки обтекания (clear, «clearfix»-хаки), борьбы со схлопыванием высоты контейнера, ручного расчёта ширин в процентах.
С появлением Flexbox и Grid эта роль float отмерла: то, ради чего его выкручивали, теперь решается напрямую и без побочных эффектов. За float осталась исходная, честная задача — обтекание врезки текстом. В новом коде использовать его для раскладки нет причин, но знать о нём необходимо: он до сих пор встречается в старых кодовых базах, и понимание его механики нужно, чтобы такой код читать и переписывать.
Flexbox: одномерный layout
Главная и поперечная оси, контейнер и элементы
Flexbox (полное название — CSS Flexible Box Layout) — первый из двух современных модулей, спроектированных специально для раскладки 4. Его назначение — распределять элементы вдоль одной оси: в ряд или в столбец. Одномерность — ключевая характеристика: Flexbox управляет раскладкой по одному направлению, оставляя перпендикулярное на втором плане.
Раскладка включается объявлением display: flex на контейнере. Его прямые потомки автоматически становятся флекс-элементами и выстраиваются вдоль главной оси (англ. main axis); перпендикулярная ей — поперечная ось (англ. cross axis). Направление главной оси задаёт свойство flex-direction: row (по умолчанию, слева направо) или column (сверху вниз). Всё поведение флекс-контейнера описывается в терминах этих двух осей, поэтому с них и стоит начинать: сначала определить, где главная ось, а затем читать остальные свойства как распределение вдоль неё и поперёк неё.
Свойства распределения: justify-content, align-items, gap
Два свойства управляют выравниванием, и различаются они осью. justify-content распределяет элементы вдоль главной оси: прижать к началу (flex-start), к концу (flex-end), по центру (center), разложить с равными промежутками (space-between, space-around, space-evenly). align-items выравнивает элементы поперёк, по поперечной оси: flex-start, flex-end, center, stretch (растянуть на всю высоту контейнера — значение по умолчанию), baseline (по базовой линии текста).
Держать в голове, какое свойство по какой оси работает, проще всего через привязку к назначению, а не к слову: justify-content — «главная», align-items — «поперечная». При flex-direction: column оси меняются местами, и теперь уже justify-content управляет вертикалью — это регулярно сбивает с толку, пока не привыкнешь думать осями, а не «горизонталью и вертикалью».
Промежутки между элементами задаёт свойство gap. До него интервалы приходилось расставлять через margin на самих элементах, что порождало возню с крайними элементами (лишний отступ у первого или последнего). gap добавляет промежутки только между элементами, не по краям, и работает как во Flexbox, так и в Grid — это одно из самых удобных приобретений современного CSS.
flex-grow, flex-shrink, flex-basis: реакция на переполнение
«Flexible» в названии — про способность элементов растягиваться и сжиматься, деля свободное или недостающее место. Поведением каждого элемента управляют три свойства.
flex-basis задаёт базовый размер элемента вдоль главной оси до перераспределения — своего рода стартовую ширину (или высоту в колонке). flex-grow — коэффициент, с которым элемент забирает избыток свободного места: при flex-grow: 0 (по умолчанию) элемент не растягивается, при 1 — растёт, деля излишек с другими растущими элементами пропорционально их коэффициентам. flex-shrink — зеркальный коэффициент сжатия при нехватке места: при 1 (по умолчанию) элемент сжимается, при 0 — сохраняет размер, отказываясь ужиматься.
На практике эти три свойства почти всегда задают одной сокращённой записью flex:
.sidebar { flex: 0 0 240px; } /* не растёт, не сжимается, ровно 240px */
.content { flex: 1 1 auto; } /* забирает всё оставшееся место */
Запись flex: 1 (эквивалент flex: 1 1 0) — самый частый идиом: «пусть элементы поделят доступную ширину поровну». Комбинацией фиксированных и гибких элементов описывается большинство панельных раскладок: боковая колонка неизменной ширины и основная область, занимающая остальное.
Типовые паттерны
Несколько задач раскладки встречаются так часто, что стали каноническими, и Flexbox решает их короче всего.
Центрирование по обеим осям — задача, которая в старом CSS требовала ухищрений, а здесь занимает три строки:
.center {
display: flex;
justify-content: center; /* по главной оси */
align-items: center; /* по поперечной */
}
Прижатый к низу футер (англ. sticky footer) — подвал, который держится у нижнего края окна, даже когда контента мало, но уходит вниз за содержимым, когда его много. Решается вертикальным флекс-контейнером на всю высоту вьюпорта, у которого основная область растягивается:
body {
min-height: 100dvh;
display: flex;
flex-direction: column;
}
main { flex: 1; } /* забирает всё свободное место, выталкивая футер вниз */
Классическая трёхколоночная раскладка (англ. holy grail) — шапка, подвал и три колонки между ними (навигация, контент, боковая панель) — тоже собирается на Flexbox, хотя для неё, как и для любой двумерной задачи, естественнее подходит Grid, к которому мы сейчас и перейдём.
Grid: двумерный layout
Линии, треки, ячейки, зоны
Grid (CSS Grid Layout) — второй модуль раскладки, и его отличие от Flexbox принципиально: Grid работает сразу в двух измерениях, управляя строками и колонками одновременно 5. Там, где Flexbox раскладывает поток элементов вдоль одной оси, Grid задаёт заранее спроектированную сетку, в ячейки которой элементы расставляются.
Терминология сетки опирается на четыре понятия. Линии (англ. grid lines) — горизонтальные и вертикальные разделители, нумеруемые с единицы (и с конца — отрицательными числами). Треки (англ. tracks) — полосы между соседними линиями, то есть колонки и строки. Ячейки — пересечения одной строки и одной колонки, наименьшая единица сетки. Зоны (англ. areas) — прямоугольные группы ячеек, которым можно дать имя и адресовать целиком.
Сетка объявляется на контейнере через display: grid и описание треков:
.layout {
display: grid;
grid-template-columns: 240px 1fr; /* колонка 240px и колонка «остальное» */
grid-template-rows: auto 1fr auto; /* шапка по содержимому, тело, подвал */
gap: 16px;
}
Здесь появляется единица fr (fraction) — доля свободного места, аналог flex-grow для сетки. grid-template-columns: 1fr 2fr делит ширину в отношении 1 : 2; 240px 1fr отдаёт первой колонке фиксированные 240 px, а второй — весь остаток. fr избавляет от ручного расчёта процентов и корректно учитывает gap.
Явная и неявная сетка; grid-template-areas
Треки, заданные через grid-template-columns и grid-template-rows, образуют явную сетку. Но элементов может оказаться больше, чем ячеек; тогда Grid достраивает неявные треки автоматически. Их размер задаётся свойствами grid-auto-rows и grid-auto-columns, а направление, в котором сетка прирастает, — grid-auto-flow. Так ведёт себя, например, лента карточек: колонок задано фиксированное число, а строки создаются по мере поступления карточек.
Размещать элементы по сетке можно двумя способами. Через номера линий — grid-column: 1 / 3 (от первой до третьей вертикальной линии, то есть на две колонки) — или, наглядно, через именованные зоны. Второй способ превращает раскладку в почти графическую схему:
.page {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
}
.page > header { grid-area: header; }
.page > nav { grid-area: sidebar; }
.page > main { grid-area: main; }
.page > footer { grid-area: footer; }
Текстовая «картинка» в grid-template-areas читается как план этажа: видно, что шапка и подвал тянутся на обе колонки, а середина разделена на боковую панель и основную область. Это одно из самых выразительных мест CSS — раскладка описывается декларативно и понятна с первого взгляда, а перестроить её под другой размер экрана можно, переписав одну эту «картинку» в медиа-запросе.
Адаптивные сетки: auto-fit, auto-fill, minmax
Особую ценность Grid раскрывает в связке функций repeat, minmax и ключевых слов auto-fit / auto-fill, дающих сетку, которая сама подстраивает число колонок под доступную ширину — без единого медиа-запроса:
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
}
Читается это так: создавай столько колонок, сколько поместится, при условии что каждая не уже 240 px и не шире равной доли остатка. На широком экране карточки лягут в четыре-пять колонок, на планшете — в две, на телефоне — в одну, и всё это описано единственным правилом. Разница между auto-fit и auto-fill тонкая и проявляется, когда элементов меньше, чем помещается колонок: auto-fit схлопывает пустые треки, растягивая имеющиеся карточки на всю ширину, а auto-fill сохраняет пустые колонки, оставляя карточки исходного размера. Этот приём — «Repeat, Auto-fit, Minmax» — настолько распространён, что стал почти шаблоном для сеток карточек, плиток и галерей.
Критерии выбора между Grid и Flexbox
Grid и Flexbox не конкуренты, а инструменты для разных задач, и выбор между ними сводится к одному вопросу: раскладка одномерна или двумерна.
Flexbox естествен, когда элементы выстраиваются вдоль одной оси и их размер должен зависеть от содержимого: панель инструментов, ряд кнопок, список тегов, горизонтальное меню. Его логика — «от контента»: элементы сами занимают столько места, сколько им нужно, и делят остаток. Grid естествен, когда нужно управлять двумя измерениями сразу или задать жёсткую структуру, в которую контент вписывается: макет страницы целиком, сетка карточек с выравниванием по строкам и колонкам, сложные формы. Его логика — «от раскладки»: сначала проектируется сетка, затем в неё расставляется содержимое.
На практике оба модуля работают вместе: Grid задаёт крупную структуру страницы, а внутри отдельных её зон Flexbox раскладывает мелкие ряды элементов. Ошибка выбора обычно ощущается как борьба с инструментом — если приходится обходными путями добиваться выравнивания по второй оси во Flexbox, это сигнал, что задача на самом деле двумерная и просит Grid.
Адаптивная вёрстка
Подход mobile-first
Одна и та же страница должна быть удобной и на телефоне шириной 360 px, и на мониторе шириной 1920 px. Подход, ставший стандартом, — mobile-first: базовые стили пишутся для узкого экрана, а стили для широких добавляются поверх, по мере роста доступного места. Причина не только в растущей доле мобильного трафика. Раскладка для узкого экрана обычно проще — один столбец, вертикальный поток, — и надстраивать над простой основой усложнения удобнее, чем упрощать сложное. Технически mobile-first выражается в том, что дополнительные правила навешиваются медиа-запросами с условием min-width: «начиная с такой-то ширины — применить». Это согласуется с идеей прогрессивного улучшения: сначала работоспособная база, затем обогащение возможностями там, где для них есть место.
Медиа-запросы и брейкпоинты
Медиа-запрос (англ. media query) — условие, при котором применяется группа правил; в теме 2, вводя prefers-color-scheme, мы обещали разобрать этот механизм здесь 6. Для адаптивной раскладки главная медиа-фича — ширина вьюпорта:
/* база: один столбец, mobile-first */
.grid { display: grid; grid-template-columns: 1fr; gap: 16px; }
/* с 768px — две колонки */
@media (min-width: 768px) {
.grid { grid-template-columns: repeat(2, 1fr); }
}
/* с 1200px — четыре */
@media (min-width: 1200px) {
.grid { grid-template-columns: repeat(4, 1fr); }
}
Ширина, при которой раскладка меняется, называется точкой разрыва (англ. breakpoint). Распространённая ошибка — привязывать брейкпоинты к диагоналям популярных устройств: модельный ряд меняется, а «айфонные» 375 px устаревают. Правильнее выбирать точки разрыва по содержимому: там, где раскладка начинает выглядеть плохо — строки становятся слишком длинными, карточки чрезмерно широкими, — там и ставить брейкпоинт, независимо от того, какому устройству эта ширина соответствует.
Container Queries как альтернатива медиа-запросам
У медиа-запросов есть принципиальное ограничение: они реагируют на размер вьюпорта, а не на размер места, отведённого конкретному компоненту. Одна и та же карточка может стоять и в узкой боковой колонке, и в широкой основной области; вьюпорт при этом один, а вот пространства у карточки в двух местах разное — и медиа-запрос эту разницу не различает. Компонент оказывается вынужден «знать» о глобальной ширине окна, хотя логично ему было бы реагировать на собственный контейнер.
Эту брешь закрывают контейнерные запросы (англ. container queries) — относительно недавнее дополнение CSS 7. Родительский элемент объявляется контейнером, а его потомки получают возможность реагировать на его размер:
.card-host { container-type: inline-size; }
@container (min-width: 400px) {
.card { grid-template-columns: auto 1fr; } /* картинка слева от текста */
}
Теперь карточка перестраивается сама, исходя из ширины отведённого ей места, где бы её ни разместили. Это сдвиг от «страница знает про все свои компоненты» к «компонент самодостаточен и заботится о себе» — то, что давно ожидалось от по-настоящему модульного, переиспользуемого интерфейса. Медиа-запросы не отменяются: они по-прежнему уместны для решений уровня всей страницы (общий каркас, поля, глобальная типографика), тогда как контейнерные запросы — для адаптации отдельных компонентов.
Гибкий layout без точек разрыва
Крайняя точка развития адаптивности — раскладка, которая почти не нуждается в явных брейкпоинтах, а плавно подстраивается сама. Такой подход называют интринсивным дизайном (англ. intrinsic web design, термин Джен Симмонс) 8. Он опирается на уже знакомые нам средства: clamp и min/max из темы 2 для «жидких» размеров, сетки на repeat(auto-fit, minmax(...)), свойство flex-wrap для рядов, переносящихся по мере нехватки места. Вместо ступенчатого переключения между фиксированными макетами на заданных ширинах раскладка непрерывно течёт, занимая ровно столько места, сколько есть.
Дисциплина здесь в том, чтобы поручать вычисление размеров самому CSS, а не перечислять вручную случаи для каждого диапазона. Правило grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)) заменяет три медиа-запроса из примера выше — и делает это устойчивее, потому что не привязано ни к каким конкретным порогам. На практике интринсивный подход и брейкпоинты сочетают: мелкую адаптацию отдают гибким единицам и авторастущим сеткам, а явные точки разрыва оставляют для немногих случаев, где раскладка должна перестроиться качественно, а не просто сжаться.
Итоги темы
Раскладка в CSS — это несколько слоёв механизмов, надстроенных друг над другом. В основании лежит нормальный поток: блочные элементы идут сверху вниз, строчные — слева направо, и большая часть контента раскладывается им без вмешательства. Позиционирование через position выводит отдельные элементы из потока и привязывает их к предку, вьюпорту или порогу прокрутки; вместе с ним приходит контекст наложения, объясняющий, почему z-index работает не так прямолинейно, как кажется. Устаревший float из инструмента раскладки вернулся к своей единственной честной роли — обтеканию врезки текстом.
Основную работу по раскладке сегодня делят два специализированных модуля. Flexbox распределяет элементы вдоль одной оси и хорош, когда размер идёт от содержимого; Grid проектирует двумерную сетку, в которую вписывается контент. Выбор между ними — вопрос размерности задачи, а не вкуса, и на практике они работают в связке: Grid держит каркас страницы, Flexbox — ряды внутри него. Адаптивность выстраивается поверх всего этого: mobile-first как отправная точка, медиа-запросы для решений уровня страницы, контейнерные запросы для самодостаточных компонентов и гибкие единицы для раскладки, которая подстраивается сама, обходясь без лишних точек разрыва. Освоив эти механизмы, можно собрать интерфейс, одинаково пригодный от телефона до широкого монитора, — что и станет задачей практического занятия 3.
Литература
- Consortium} {. W. W. Cascading Style Sheets Level 2 Revision 1 (CSS 2.1) Specification. — 2011, https://www.w3.org/TR/CSS21/.
- Consortium} {. W. W. CSS Display Module Level 3. — 2023, https://www.w3.org/TR/css-display-3/.
- Consortium} {. W. W. CSS Positioned Layout Module Level 3. — 2023, https://www.w3.org/TR/css-position-3/.
- Consortium} {. W. W. CSS Flexible Box Layout Module Level 1. — 2018, https://www.w3.org/TR/css-flexbox-1/.
- Consortium} {. W. W. CSS Grid Layout Module Level 2. — 2023, https://www.w3.org/TR/css-grid-2/.
- Consortium} {. W. W. Media Queries Level 5. — 2024, https://www.w3.org/TR/mediaqueries-5/.
- Consortium} {. W. W. CSS Containment Module Level 3. — 2024, https://www.w3.org/TR/css-contain-3/.
- Bell A., Pickering H. Every Layout: Relearn CSS Layout. — 2023, https://every-layout.dev/.