Коробка и поток: отступы, схлопывание, em и rem

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

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

Пока раскладку никто не задавал, браузер всё равно расставляет элементы: блоки встают друг под другом, каждый во всю ширину, между ними просветы. Это нормальный поток — правило, по которому стоят коробки, пока их не переставили flex, grid или position. У потока есть привычки, которые удивляют: отступы 30 и 20 пикселей дают просвет 30, а не 50; отступ ребёнка сдвигает родителя целиком; а блок с width: 200px на одной странице занимает 200 пикселей, а на другой — 250. Разберём коробку по слоям, посмотрим, как блоки ведут себя в потоке, и выясним, от чего на самом деле считаются единицы em и rem.

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

Каждый элемент браузер рисует прямоугольником — коробкой. Как коробки встают относительно друг друга, решает свойство display. Два его значения уже встречались в первой главе: block — отдельной полосой, inline — внутри строки, как слово. Пока раскладку не меняли, блоки подчиняются нормальному потоку: каждый следующий встаёт под предыдущим, в том порядке, в каком они записаны в разметке.

Проверим на колонке шириной 200 пикселей с тремя блоками высотой 30, 50 и 20. Ширина задана только второму — 120, и справа от него остаётся 80 свободных пикселей. У самой колонки высоты нет. Прогноз: где встанет третий блок — справа от второго или под ним? Какой ширины окажутся первый и третий и какой высоты вся колонка?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Третий блок встал под вторым, на 80 = 30 + 50, хотя справа от второго место было. Блок в потоке всегда начинается с новой строки, сколько бы места ни оставалось рядом. Ширина без width — вся ширина родителя: у первого и третьего по 200. Высота без height считается по содержимому: 30 + 50 + 20 = 100.

Ширина блока приходит сверху, от родителя, а высота — снизу, от содержимого. Вопрос «почему блок такого размера» почти всегда решается этой парой.

Строчные элементы — <span>, <a>, <em> — живут по другим правилам. Они встают в строку друг за другом, как слова, и переносятся на следующую, когда место кончается. На строчный элемент с текстом width и height не действуют: его размер задаёт текст. Дальше в статье речь о блоках.

Коробка блока состоит из четырёх слоёв, от середины наружу:

СлойСвойствоЧто делает
содержимоеwidth, heightместо для текста и детей
внутренний отступpaddingотодвигает содержимое от рамки; фон блока заливает и его
рамкаborderлиния вокруг внутреннего отступа: border: 5px solid — сплошная толщиной 5 пикселей
внешний отступmarginпрозрачное поле снаружи рамки, которое отодвигает соседей

Одно значение, как в padding: 20px, задаёт отступ со всех четырёх сторон. Для одной стороны есть отдельные свойства — margin-top, margin-bottom, padding-left и так далее, — и каждое из них можно спросить по имени, не спрашивая остальные.

У карточки ниже ширина 200, внутренний отступ 20, рамка 5 и внешний отступ 15. Внутри — блок высотой 30 без ширины. Прогноз: какой высоты будет карточка, какой ширины блок внутри и на каком y он начнётся?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Высота карточки — 80: 30 содержимого, по 20 внутреннего отступа сверху и снизу и по 5 рамки. Блок внутри начинается на 40: 15 внешнего отступа карточки, 5 рамки и 20 внутреннего отступа. А ширина у блока 150 = 200 − 20 − 20 − 5 − 5. Внутренний отступ и рамка поместились внутрь 200 пикселей и отняли место у содержимого, а высота карточки, которую никто не задавал, росла наружу.

Так считает песочница учебника, но не CSS по умолчанию. Что входит в width, решает свойство box-sizing. Две карточки ниже одинаковы во всём, кроме одного: второй явно сказано box-sizing: content-box. Прогноз: какая ширина окажется у каждой — и почему они разные при одинаковой разметке?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
У первой 200, у второй 250. При content-box width — ширина одного содержимого, а внутренний отступ и рамка прибавляются снаружи: 200 + 20 + 20 + 5 + 5. При border-box width — ширина всей коробки по внешнему краю рамки, и содержимому достаётся то, что останется. С height то же самое. Внешний отступ не входит в размер ни в одной модели: он всегда снаружи.

По стандарту умолчание — content-box. Откуда тогда border-box у первой карточки, в стилях которой о нём ни слова? Его ставит правило для * — то самое, чей селектор напечатал rules() в разборе сброса стилей песочницы. На вашей странице без такого сброса width: 200px с теми же отступами и рамкой даст коробку 250.

Блок шире, чем написано в width? Проверьте box-sizing: при content-box к ширине прибавляются padding и рамка.

Симптом, с которым это всплывает в жизни, ровно тот, что обещан в лиде: вы написали width: 200px, добавили внутренний отступ 20 и рамку 5 — и блок занял 250 пикселей, сломав раскладку в три колонки. При content-box ширина описывает содержимое, а отступы и рамка прибавляются снаружи; при border-box в ширину входит всё, и 200 остаются 200. Поэтому сброс с border-box для всех элементов стоит почти в каждом проекте: с ним размеры в разметке и в голове совпадают.

Стандарт CSS1 в 1996 году записал: width — ширина содержимого. Но Internet Explorer 5 под Windows, как и его предшественники, считал её от рамки до рамки, вместе с внутренним отступом. Internet Explorer 6 в 2001 году перешёл на стандартную модель, а старую оставил для страниц в режиме совместимости. Годами одна и та же ширина давала в разных браузерах разные коробки.

Старая модель при этом оказалась удобной, и стандарт вернул её как выбор: так появилось свойство box-sizing со значением border-box. Самый частый повод — поле во всю ширину формы. В форме шириной 200 два поля ввода с width: 100%, внутренним отступом 10 и рамкой 2, у первого стандартная модель. Прогноз: какой ширины будет каждое поле?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Первое — 224: 100% достались содержимому, и отступы с рамкой вылезли за край формы на 24 пикселя. Второе — ровно 200. Пока в CSS не было функции calc(), при content-box такое поле нельзя было записать вовсе: «100% минус 24 пикселя» одним числом не выразить. 1 февраля 2012 года Пол Айриш предложил в своём блоге ставить border-box сразу всем элементам, и правило для * разошлось по сбросам. С тех пор это обычная строка сброса, как и в песочнице учебника.

В части II те же слои читает JavaScript: у элемента есть отдельные числа для ширины вместе с рамкой и для ширины без неё. В примерах, где их меряют, box-sizing: content-box стоит явно. Теперь понятно зачем: без него песочница посчитала бы коробку по своему сбросу, и арифметика не сошлась бы. Эти числа разбирает статья о размерах элемента: offset, client и scroll.

Внешние отступы соседних блоков ведут себя неожиданно. Ниже два блока высотой 40: у первого внешний отступ снизу 30, у второго сверху 20. Прогноз: на каком y начнётся второй блок?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
На 70, а не на 90. Отступы 30 и 20 не сложились, а схлопнулись в один — больший из двух: 40 + 30 = 70. Так браузер обращается с вертикальными отступами соседних блоков в нормальном потоке. Смысл правила виден на тексте: у абзаца в листе стилей браузера отступы сверху и снизу по 1em, и между двумя абзацами получается просвет в 1em, а не в 2em.

Схлопываются только вертикальные отступы, горизонтальные складываются всегда. И только в потоке: у flex-элементов, до которых глава ещё дойдёт, отступы не схлопываются вовсе.

На практике схлопывание чаще всего отвечает на вопрос «почему просвет между карточками 20, а не 40». У каждой карточки внешний отступ сверху и снизу по 20, и соседние отступы не складываются, а выбирают больший. Это же объясняет, почему первый элемент списка иногда уезжает от заголовка дальше, чем задумано: его верхний отступ схлопывается с отступом контейнера и вылезает наружу — следующий раздел как раз про это.

Схлопываются не только соседи. Верхний отступ первого ребёнка схлопывается с верхним отступом родителя, если между ними нет ни внутреннего отступа, ни рамки. Отступ оказывается снаружи родителя и сдвигает его целиком. С нижним отступом последнего ребёнка происходит то же, если высота родителя не задана. Ниже два блока .wrap, в каждом ребёнок высотой 20 с отступом сверху 25. Второму блоку добавлен display: flow-root. Прогноз: на каких y начнутся оба блока и оба ребёнка?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Первый блок начался на 25, и его ребёнок — тоже на 25. Отступ ребёнка вытек наружу и отодвинул родителя, а внутри родителя ребёнок прижат к верхнему краю. Высота первого блока — 20, он кончается на 45. Второй блок начался сразу за ним, на 45, а его ребёнок — на 70: отступ остался внутри. Это и сделал flow-root.

display: flow-root делает блок корнем собственного потока: отступы детей больше не схлопываются через его край и остаются внутри. Снаружи он остаётся обычным блоком потока. Внутренний отступ или рамка у родителя тоже останавливают отступ, но меняют размер родителя, а flow-root — нет. display: flow-root — Baseline widely с 15.07.2022, пишите.

Сцена, на которую пример кладёт вашу разметку, сама стоит с display: flow-root. Иначе отступ первого блока вытекал бы из неё, и y считался бы не от того края. Этим объясняется число из раздела о слоях: блок внутри карточки начался на 40, и в эти 40 вошли 15 внешнего отступа самой карточки.

Отступ первого ребёнка сдвинул весь родительский блок? Отступ вытек: дайте родителю display: flow-root.

Свойство float: left вынимает блок из потока и прижимает к левому краю, а текст соседей обтекает его справа. Придумали его для обтекания картинок, но в 2000-х на нём раскладывали целые страницы: колонки были плавающими блоками. У этого была цена: родитель не видит плавающих детей и считает высоту только по тем, кто остался в потоке. Ниже контейнер с одним плавающим блоком высотой 100 и такой же контейнер с display: flow-root. Прогноз: какой высоты окажется каждый?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
У первого 0: плавающий ребёнок торчит из пустого контейнера вниз и наезжает на то, что идёт следом. Чинили это приёмом clearfix. CSS дорисовывал в конец контейнера невидимый элемент со свойством clear: both — оно запрещает стоять рядом с плавающими блоками. Такой элемент опускался ниже плавающего ребёнка, и контейнер растягивался до него. Самый известный вариант — микро-clearfix Николаса Галлахера от 21 апреля 2011 года: несколько строк, которые заодно добавляли невидимый элемент и в начало контейнера, чтобы не вытекал верхний отступ первого ребёнка. Был и способ проще — overflow: hidden у родителя: он удерживал плавающих детей, но обрезал всё, что выходит за край.

display: flow-root заменил весь приём одним свойством: у второго контейнера высота 100. В Chrome 58 и Firefox 53 оно вышло 19 апреля 2017 года. Сам float при этом не устарел — устарели раскладки на нём. Он остаётся инструментом обтекания: картинка слева, текст вокруг. А свойство shape-outside, которое задаёт форму обтекания, например круг, работает только у плавающего элемента.

Из потока элемент можно и вынуть совсем. position: absolute ставит его по координатам, а соседи смыкаются, будто его нет. От какого края считаются эти координаты и почему значок иногда улетает из карточки, разбирает следующая статья главы — позиционирование: сдвиг и якорь.

Отступы не обязательно писать в пикселях. Единица em равна размеру шрифта. Вы встречали её в листе стилей браузера: у заголовка font-size: 2em и отступы по 0.67em. Правило оттуда стоит повторить: в самом font-size em — размер шрифта родителя, во всех остальных свойствах — размер шрифта самого элемента. Единица rem устроена проще: это размер шрифта корня страницы, элемента <html>, где бы ни стоял элемент.

У карточки шрифт 20px, у абзаца внутри свой шрифт не задан. Внутренний отступ абзаца слева — 1em, внешний — 1rem. Прогноз: сколько пикселей в каждом?

js
разметка страницы
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
padding-left — 20: абзац унаследовал шрифт карточки, и 1em равен ему. margin-left — 16, и ни 20 карточки, ни 14 песочницы тут ни при чём. Песочница задаёт шрифт 14px элементу <body>, а у <html> размер шрифта не задан никем. Он приходит из настроек браузера, а там по умолчанию стоит 16px.

Если в настройках браузера другой размер шрифта, пикселей выйдет иначе — и у вас, и в разборе. Это не поломка, а смысл rem. Масштаб страницы — Ctrl и плюс, в macOS Cmd и плюс — увеличивает всё, и пиксели тоже. А размер шрифта в настройках меняет только размеры, которые от него считаются: в rem и в em, если выше по дереву никто не задал шрифт в пикселях. Человеку, который выбрал крупный шрифт, чтобы легче читать, текст в rem подчинится, а текст в пикселях — нет.

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

Главная ловушка em — вложенность. font-size: 0.75em у элемента внутри такого же элемента даёт три четверти от трёх четвертей, и каждый следующий уровень мельче предыдущего: на четвёртом уровне текст уже не прочитать. Отсюда правило, которое экономит часы: размеры шрифта пишут в rem, а в em — только то, что должно расти вместе со шрифтом своего элемента, например внутренний отступ кнопки. Ловушку вложенности вы посчитаете во второй задаче.

  • В нормальном потоке блоки встают друг под другом в порядке разметки, даже если рядом есть место. Ширина без width — вся ширина родителя, кроме собственных внешних отступов по бокам, высота без height — по содержимому.
  • Коробка — четыре слоя: содержимое, внутренний отступ padding, рамка border и внешний отступ margin. Внешний отступ не входит в размер ни в одной модели.
  • При content-box, умолчании CSS, width — только содержимое, а padding и рамка прибавляются снаружи. При border-box width — вся коробка по краю рамки. Сброс песочницы ставит border-box всем элементам.
  • Вертикальные отступы соседних блоков в потоке схлопываются до большего: 30 и 20 дают 30. Горизонтальные складываются.
  • Верхний отступ первого ребёнка вытекает из родителя, у которого нет padding и рамки. display: flow-root оставляет его внутри и заодно удерживает плавающих детей.
  • em в font-size — шрифт родителя, в остальных свойствах — шрифт самого элемента. rem — шрифт корня страницы, поэтому он следует за размером шрифта в настройках браузера: 16px — только при стандартной настройке.

1/4У блока margin-bottom: 40px, у соседа под ним — margin-top: 10px. Какой просвет между ними в нормальном потоке?

2/4У блока box-sizing: content-box, width: 100px, padding: 10px и border: 2px solid. Какую ширину покажет прибор?

3/4У карточки font-size: 20px, у абзаца внутри — font-size: 1.5em и margin-top: 1em. Чему равен отступ абзаца сверху?

4/4Верхний отступ первого ребёнка сдвинул вниз весь родительский блок. Что оставит отступ внутри, не меняя размеров родителя?

Карточки стоят теснее, чем задумано

В каждой карточке заголовок и текст — пустые блоки заданной высоты. Сначала, не запуская пример, посчитайте, на каких y прибор покажет два заголовка, и только потом запустите. Затем допишите одно объявление в правило .card, чтобы верхний отступ заголовка оставался внутри своей карточки.

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

Первый заголовок — на 16: его отступ вытек из карточки, у которой нет ни внутреннего отступа, ни рамки. Текст — на 16 + 30 + 12 = 58: отступы 8 и 12 между заголовком и текстом схлопнулись до 12. Текст кончается на 98. Дальше встречаются сразу четыре отступа: у текста снизу 0, у первой карточки снизу 20, у второй карточки сверху 0 и у второго заголовка сверху 16. Все они схлопываются в один, 20, и второй заголовок стоит на 118.

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

Прибор напечатает .title → y: 16px, 134px. Первый заголовок остался на 16, но теперь его отступ внутри карточки. Вторая карточка начинается на 98 + 20 = 118, и отступ её заголовка отсчитывается уже внутри: 118 + 16 = 134.

Меню, которое мельчает

Во вложенном меню пункты мельче, чем во внешнем. Посчитайте до запуска размер шрифта и отступ слева у обоих меню. Потом поменяйте в правиле одну единицу, чтобы шрифт у обоих меню стал одинаковым, а отступ слева по-прежнему равнялся шрифту своего меню.

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

Внешнее меню: 0.75 × 20 = 15px, отступ 1em — тоже 15px. Вложенное берёт три четверти уже от 15px: 11.25px, и отступ вслед за ним. Каждый следующий уровень умножал бы ещё на 0.75.

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

При стандартной настройке шрифта 16px прибор напечатает .menu → font-size: 12px, 12px и .menu → padding-left: 12px, 12px. rem считается от корня страницы, и вложенность на него не влияет. А padding-left остался в em: отступ следует за шрифтом своего меню, каким бы тот ни был.

Посчитайте до запуска

У двух заметок шрифт 20px, ширина 200 пикселей, внутренний отступ 0.5em и рамка 2 пикселя. Вторая заметка работает по стандартной модели. Внутри каждой — пустой блок .line без ширины. Не запуская пример, посчитайте четыре числа: ширину обеих заметок и обоих блоков. Потом запустите.

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

Внутренний отступ — 0.5em от шрифта самой заметки: 0.5 × 20 = 10px с каждой стороны. Первая заметка живёт по сбросу песочницы, border-box: её ширина ровно 200, а блоку внутри остаётся 200 − 10 − 10 − 2 − 2 = 176. У второй content-box: 200 — это её содержимое, поэтому блок внутри получает все 200, а сама заметка — 200 + 10 + 10 + 2 + 2 = 224. Прибор напечатает .note → width: 200px, 224px и .line → width: 176px, 200px.