Удивительный и неизвестный inline-block

Эта статья задумана как начало цикла про загадки, сюрпризы, малоизученные особенности и маленькие полезные секреты самого могущественного, пожалуй, свойства в CSS — свойства display. У нас уже были статьи про новинки этого свойства — значения contents (что это вообще, его сюрпризы, преимущества и подводные камни для доступности) и flow-root. Но и старые, давно знакомые (казалось бы) значения этого свойства таят в себе немало сюрпризов, подводных камней и удивительных открытий. Прежде всего, пожалуй, это значения с «приставкой» inline-: inline-flex, inline-grid, inline-table, и, наконец, наш старый знакомый inline-block.

С него-то мы и начнем наш экскурс в фундаментальные глубины CSS образца 2018 года, в конце которого у нас должно сложиться целостное представление обо всех существующих механизмах раскладки в браузере. Готовы к сюрпризам?

Чуть-чуть истории

Для молодых верстальщиков и фронтендеров инлайн-блоки — что-то очень древнее. Но ни в CSS1, ни в CSS2 никакого inline-block не было. Появился он только в CSS2.1, а стандартом CSS2.1 стал лишь в 2011-м, в эпоху «CSS3». В Firefox до 3-й версии (а версии тогда «жили» не по полтора месяца, а годами) inline-block приходилось эмулировать через -moz-inline-box, который был не чем иным, как… мозилловской реализацией старой версии флексбоксов (да-да, инлайн-блоки эмулировали флексбоксами, вот так ирония!). Как ни странно, IE это значение поддерживал (в нем оно впервые и появилось!), но… только для элементов с дефолтным inline (впрочем, это ограничение обходилось простым хаком). В общем веселое было время:)

В начале 2010-х инлайн-блоки пережили всплеск популярности как основа для сеток. Они давали намного больше простора для разных вариантов выравнивания, чем надоевшие флоаты, и при этом не нуждались ни в каких «клиарфиксах». И по сей день они — один из самых популярных фолбэков для нормальных систем раскладки (флексбоксов и гридов).

Но как и флоаты, инлайн-блоки не были предназначены для раскладки блоков, так что своих хитрых хаков и тут хватало. Не случайно две статьи про такие хаки до сих пор у нас в числе самых популярных.

Не inline и не block

Во многих учебниках и руководствах для новичков про inline-block пишут, что он «строчный и блочный одновременно». Это неправда.

Inline-block — не inline-элемент (который может разрываться между строками, для которого вертикальные padding-и и border-ы не влияют на его положение, который игнорирует width и height и т.д.). И не блочный элемент (который пытается заполнить всю доступную ширину контейнера, margin-ы которого схлопываются с margin-ами соседей и даже потомков, в который могут вторгаться флоаты и т.д.). У него есть отдельные черты того и другого, но…

Пожалуй, проще всего понять, что такое inline-block на самом деле, обратившись к новой спецификации CSS Display 3 уровня.

Постоянные читатели нашего сайта уже знают, что по новой спецификации свойство display отвечает за две вещи (на самом деле не только, но пока хватит этого):

  1. Внешнее поведение элемента — как он взаимодействует с соседями, т.е. в каком контексте форматирования он сам находится (или, как говорит спецификация, участвует);
  2. Внутреннее поведение элемента — как он влияет на размещение своих потомков, т.е. какой контекст форматирования действует в нем самом.

Для каждого поведения теперь есть отдельный набор ключевых слов, так что значения свойства display по новому стандарту будут составными. А старые значения с дефисом будут псевдонимами, алиасами для некоторых из таких комбинаций (говорю «будут», потому что браузеры еще не понимают составных значений, но в стандарт это уже заложено).

Какому же составному значению будет псевдонимом наш inline-block? Думаете, «inline block» («инлайновый снаружи, блочный внутри»)? Вот и нет! На самом деле его полное значение вот какое:

inline flow-root

Иначе говоря, он участник инлайнового контекста снаружи и начало нового блочного контекста форматирования внутри. Неожиданно! Но логично.

В инлайновом контексте форматирования ничего блочного быть не может. По построению. Как только в контейнере появляется что-то с display:block, контейнер по волшебству — то есть по стандарту — превращается из контейнера текста в контейнер блоков, и у нового блока появляются анонимные блочные соседи. Поэтому, чтобы вставить блочное содержимое в инлайновый контекст, нужен некий «переходник». Начало блочного контекста форматирования, ведущее себя как элемент строки — это он и есть. По-видимому, редакторы спецификаций сами поняли это только с появлением самого flow-root. Из-за этого «запоздалого прозрения» пришлось спешно решать еще одну проблему… но о ней чуть позже, в продолжениях.

То, что он сам участвует в инлайновом контексте — ключевая особенность инлайн-блока. Из нее вытекают и многие его преимущества, и почти все раздражающие недостатки. Они, кстати, будут и у inline-table, inline-flex и inline-grid, так что это знание пригодится во многих ситуациях.

Атомарный элемент строки

За одним редким исключением (о нем в другой раз), все «переходники» между инлайновым и каким-либо другим контекстами форматирования ведут себя как сплошные прямоугольники, при этом встроенные в строку текста, как буквы или эмодзи. Кстати, так же себя ведут и «переходники» между текстом и чем-то, вообще не относящимся к CSS — замещаемые элементы (картинки, видео…) и виджеты (напр. элементы форм), как их называют в HTML5. CSS называет такие неделимые прямоугольники атомарными элементами строки (atomic inline boxes).

Ключевой момент здесь то, что это элементы строки. А значит, подчиняются непростым для понимания правилам инлайнового контекста:

See the Pen Приключения инлайн-блоков в строчном контексте форматирования by Ilya Streltsyn (@SelenIT) on CodePen.

Вот несколько особенностей, которые, по моим наблюдениям, вызывают больше всего проблем и недоразумений на практике.

Минимальное пространство по высоте

Любой инлайн-блок, даже самый маленький, заставляет браузер использовать целый контейнер строки, минимальная высота которого определяется свойством line-height родителя. Увеличить (распереть) эту высоту инлайн-блок может, но уменьшить — нет. А поскольку высота строки складывается из двух частей — над базовой линией и под ней, возможна ситуация, когда инлайн-блок «распирает» верхнюю часть, «отпихивая» собой базовую линию от «потолка» контейнера строки (как пустой инлайн-блок в нашем примере), но пространство под ним остается! Даже если крошечный инлайн-блок будет единственным потомком родителя, он всё равно займет целую строку.

Так что, если у крошечной кнопки или иконки вдруг появились «непонятные отступы», которых не видно даже в браузерном отладчике (!), просто вспомните про эту особенность. Побороть ее можно, задав контейнеру очень маленькую line-height (вплоть до 0). Но лучше сразу использовать для таких вещей не инлайн-блоки, а обычные блоки или флексбоксы — там ни с чем бороться не надо.

Пробелы имеют значение

Про эту «проблему», думаю, читатели нашего сайт давно знают (см. вторую по популярности статью:). Но упомянуть ее надо. Коротко: в инлайновом контексте любые пробельные символы подряд (сами пробелы, табуляции и переводы строки, в т.ч. между тегами) внутри текста, при дефолтном white-space: normal, отображаются на экране как один пробел. В примере выше видно, почему это не баг: ведь инлайн-блоки вписываются в текст. А значит, отделяются пробелами от соседних слов на общих основаниях и прижимаются вплотную к знакам препинания и другим символам, если надо. Если нужно другое поведение, лучшее решение — использовать что-то другое. Не инлайн-блоки, а, например, те же флексбоксы.

Здесь же стоит упомянуть, что по умолчанию инлайн-блок может переноситься на новую строку, даже если перед ним нет пробела. Так сложилось исторически. Если нужно «приклеить» инлайн-блок к соседнему слову, чтоб они переносились только вместе (например, иконку к ссылке), можно, например, обернуть их в общий <span> с white-space: nowrap или вставить между ними неразрывный символ типа &nbsp;.

«Странное» действие middle

В последней строке примера видно, что второй инлайн-блок, с vertical-align: middle, не выровнен по середине контейнера строки, а стоит гораздо ниже. Можно подумать, что vertical-align вообще не сработало, но если присмотреться, видно, что текст в нем на пиксель ниже общей базовой линии.

Разгадка в том, что middle выравнивает середину высоты инлайн-блока с серединой высоты строчных букв текста родительского элемента. Т.е. ставит ее на 0.5ex выше базовой линии. Середина контейнера строки с этой точкой обычно не совпадает. Поэтому такое выравнивание сдвигает инлайн-блок и, как часто бывает в строчном контексте, может дополнительно «распереть» контейнер строки и вызвать еще один непонятный отступ над (или под) элементом. Будьте начеку!

Уникальная базовая линия

Эту особенность я даже вынес в отдельный раздел, потому что она (на сегодняшний день) уникальна именно для инлайн-блоков: их базовой линией, по которой они выравниваются относительно окружающего текста, считается базовая линия последней строки текста внутри них. Это видно на примере «инлайн-блока в две строки» выше. У элементов с другими display: inline-что-то (о них мы позже поговорим отдельно) базовая линия берется не по последней, а по первой строке. Чуть ниже мы увидим, как эта уникальность может нам пригодиться.

Важный нюанс: если контента в инлайн-блоке нет, то базовой линией считается нижняя граница его внешнего отступа (см. последний инлайн-блок в примере). Если его overflow не равно visible — тоже. Так что если под пустыми или обрезанными инлайн-блоками появляется больше пустого места, чем под непустыми — это не баг (если мешает, можно просто поменять vertical-align). Уникален ли этот нюанс для инлайн-блоков, или другие display: inline-* должны его перенять — пока неясно🙂

inline-block в роли блока

И всё же многим атомарный элемент строки очень похож на блок. У него есть все элементы боксовой модели (margin/border/padding), и все они работают предсказуемо, увеличивая занимаемое элементом место (как мы опять же видели в примере). У него есть размеры. В частности, ему можно задать width: 100% (минус боковые padding-и, если надо) — и он растянется на всю доступную ширину контейнера. И внутри у него может быть полноценное блочное содержимое — заголовки, абзацы, даже флоаты и таблицы (если надо). Чем не блок? Но всё же существенная разница есть.

Неуправляемая анонимная блочная обертка

Контейнер строки, в котором живет инлайн-блок и который нельзя сделать ниже определенного минимального размера, никуда не девается. И если рядом с этим контейнером строки оказывается что-то блочное, то он оборачивается в анонимный блок. То есть на самом деле у нас получается этакая «матрёшка»: снаружи анонимный обычный блок (с параметрами по умолчанию — на всю доступную ширину родителя, отступы по нулям и т.п.). В нем — строка «виртуального» текста, со свойствами, унаследованными от родительского контейнера. Внутри этой строки — атомарный элемент на всю ее ширину. И только внутри него — отдельный блочный контекст форматирования со своим блочным содержимым.

Анонимный блок всегда продолжает родительский контекст форматирования, поэтому «ужиматься», чтобы разместиться рядом с флоатом, не умеет. Сам инлайн-блок — атомарный элемент строки, как длинное неразрывное слово — тоже. Поэтому, если контента в нем много, инлайн-блок будет стремиться заполнить всю ширину родителя, а не только свободное место рядом с флоатом. Это еще одно важное отличие инлайн-блока от обычного, блочного flow-root.

See the Pen Сравнение обычного flow-root и инлайн-блока (inline flow-root) в блочном окружении by Ilya Streltsyn (@SelenIT) on CodePen.

Много строк внутри — одна строка снаружи

Еще раз отметим, что с точки зрения родительского блочного контейнера инлайн-блок в своей анонимной блочной обертке считается единственной строкой «призрачного» текста. В некоторых случаях это бывает полезно (см. ниже).

Полезные применения inline-block

Тривиальное

Прямое назначение инлайн-блоков — вставка чего-то «блокоподобного» (кнопки, текстовые иконки, теги, беджи и т.п.) прямо в текст.

See the Pen wxdavy by Ilya Streltsyn (@SelenIT) on CodePen.

Хак: фолбэк для флексбоксов (и гридов)

С оговорками, но всё же у текста в CSS есть аналоги многих возможностей флексбоксов. Включая многие виды горизонтального (влево, вправо, центр, по ширине) и вертикального (по базовой линии, по верхнему или нижнему краю контейнера строки…) выравнивания. Поэтому инлайн-блоки издавна используют как своего рода «флексбоксы (а то и гриды) для бедных».

Но они не владеют главным «волшебством» самого свойства flex (автоматический размер) и не умеют подстраиваться к высоте соседей. И, наверное, не стоит тратить время и силы, пытаясь воспроизвести в фолбэке все красоты современного решения. Если в ископаемом браузере, который к тому же наверняка работает на ископаемом же «железе», блоки просто выстроены по горизонтали в цепочку, и макет не разваливается — его неизбалованному пользователю этого достаточно.

Хак: еще одна альтернатива клиарфиксу

Как любой элемент, создающий блочный контекст форматирования, inline-block не выпускает наружу ни «плавающих» потомков, ни margin-ов обычных, всегда охватывая их по габаритам. Поэтому его в принципе можно использовать «вместо клиарфикса». Но помните об издержках неуправляемой анонимной обертки. И лучше используйте «нормальный», блочный flow-root. А где его нет — подбирайте наиболее подходящий для вас вариант из сравнительной таблицы (мой фаворит — column-count: 1;).

Хак: запрет переноса строк в колонках

В многоколоночной раскладке по умолчанию блоки могут разрываться и переноситься в новую колонку по частям. Обычно так и надо, но иногда нужно какой-то блок перенести только целиком. Есть давний хак, когда для этого таким блокам меняют display на inline-block. Теперь вы знаете, благодаря чему он работает: ведь для блочного контекста весь анонимный блок с инлайн-блоком внутри — это одна строка!

Хак: выравнивание флекс- или грид-элементов по последней строке заголовка

Бывают задачи, которые пока не под силу даже могучим свойствам для выравнивания всего, но решаются благодаря уникальной базовой линии инлайн-блоков. Иногда они могут почти заменить подсетки:

See the Pen MBmwRw by Ilya Streltsyn (@SelenIT) on CodePen.

Допустим, маркетолог вот такого каталога требует, чтобы товары в нем были непременно выровнены по низу заголовков (или по верху описаний, что то же самое). Первое, что приходит на ум — таблица, либо грид с разбивкой заголовка и текста в разные грид-элементы. Но если сделать весь header каждого товара инлайн-блоком, этот инлайн-блок окажется единтственным содержимым первой как-бы-текстовой строки, и ее базовая линия пройдет по последней строке этого инлайн-блока. Теперь достаточно выровнять сами товары по этой базовой линии, т.е. align-items: baseline — и вуаля.

Примечание: к сожалению, в Firefox с выравниванием в гриде нашелся странный баг. Я долго не мог воспроизвести его отдельно, потому что задавал заголовку явную ширину (чтоб форсировать перенос на новую строку), а оказалось, что эта ширина (не в процентах, и не больше 100% по факту) тот баг… почему-то фиксит:). Так что в примере как раз применен этот «фикс», но возможности метода это изрядно ограничивает. Зато со флексбоксами никаких подобных проблем пока не нашлось, что радует.

Миф об «инлайн-блочных элементах в HTML»

Напоследок — небольшой курьез. С развитием CSS старинное деление HTML-элементов по внешнему виду, на «блочные» и «строчные», стало порождать домыслы о том, что другим видам CSS-отображения тоже могут соответствовать какие-то свои категории контента в HTML. Так, видимо, родился миф об «инлайн-блочных элементах в HTML». Чаще всего в таковые «записывают» элемент <button>, реже <img> и др. Так вот, это ерунда.

В HTML не было и нет такой категории элементов, и выделять ее нет смысла, полезной информации она не даст. Классифицируйте элементы по их логической роли: структурные, заголовочные, текстовые, интерактивные и т.д., как это делают современные стандарты. Эта классификация сразу даст вам подсказку, для чего элемент может использоваться (например, структурный — для разметки крупных логических блоков страницы, чтобы пользователю скринридера было удобно легко переходить между ними, поэтому их не должно быть на странице слишком много) и что в него можно вкладывать (скажем, интерактивные элементы обычно нельзя вкладывать друг в друга, чтобы браузер не запутался, чьё действие выполнять). А внешний вид любому (ну, почти) элементу можно придать любой — с помощью CSS. В том числе и вид инлайн-блока.

В CSS — и в этой статье — термины «блок» и «строчный элемент»/«элемент строки» относятся исключительно к внешнему виду, и могут относиться вообще не к элементу DOM, а к псевдоэлементу. Общего с устаревшей классификацией элементов из HTML 4 у них не больше, чем у JavaScript и Java. Не дайте себя запутать, и да пребудет с вами CSSила! 🙂

P.S. Это тоже может быть интересно: