Line-height и vertical-align — это простые CSS свойства. Они настолько просты, что большинство из нас убеждено в том, что в полной мере понимают, как они работают и как их использовать. Но это не так. В действительности же они сложны. Можно даже считать, что они являются самыми сложными свойствами, т.к. они играют важную роль в создании мало известной особенности CSS: в строчном форматировании текста.
Например, line-height может быть установлен в качестве длины или безразмерного значения, но по умолчанию у него задано значение normal. Окей, но что такое normal? Пишут, что чаще всего это 1 или может даже 1.2. Но даже в спецификации по CSS нет ясного ответа на этот вопрос. Мы знаем, что line-height — безразмерное свойство относительно родительского значения font-size, но проблема заключается в том, что font-size: 100px
выглядит по разному, в зависимости от семейства шрифта. Вопрос: значение line-height всегда одинаковое или разное? Действительно ли значение варьируется между 1 и 1.2? А vertical-align? Каково его влияние в отношении line-height?
Глубокое погружение в CSS не такой простой механизм…
Давайте сперва поговорим о font-size
Посмотрите на простой HTML код. Тег p
содержит три span
, каждый из которых имеет свой собственный font-family
:
<p>
<span class="a">Ba</span>
<span class="b">Ba</span>
<span class="c">Ba</span>
</p>
p { font-size: 100px }
.a { font-family: Helvetica }
.b { font-family: Gruppo }
.c { font-family: Catamaran }
Используя некоторое значение font-size с различными font-family, мы получаем элементы разной высоты:
Почему font-size: 100px
не создает элементы с высотой в 100px? Я измерил и получилось:
- Helvetica — 115px;
- Gruppo — 97px;
- Catamaran — 164px.
Все это кажется немного странным, на первый взгляд, но это вполне объяснимо. Причина находится внутри самого шрифта. Вот как это работает:
- шрифт определяется его em-квадратом (или единицей em) своего контейнера, в котором будет нарисован каждый символ. Этот квадрат использует относительные единицы и обычно устанавливает значение на уровне 1000 единиц. Но здесь также может применяться значение 1024, 2048 и т.д.
- устанавливается на основе своих относительных единиц, показателей шрифтов (ascender, descender, capital height, x-height и т.д.). Обратите внимание, что некоторые значения могут выходить за пределы em-квадрата.
- в браузере относительные единицы масштабируются, чтобы нужный размер соответствовал шрифту.
Давайте возьмем шрифт Catamaran и откроем его в FontForge, чтобы получить его показатели:
- em-квадрат 1000;
- Ascender — 1100 и Descender — 54. После выполнения некоторых тестов, окажется, что браузеры используют значения HHead Ascent/Descent на Mac OS, а также значения Ascent/Descent на Windows (эти значения могут отличаться!). Отметим также, что прописная буква имеет высоту 640px и высота обычной буква составляет 485px.
Это значит, что шрифт Catamaran использует 1100 + 540 единиц при 1000 единиц в em-квадрате, который дает высоту 164px, в то время, как в настройках указано font-size: 100px
. Эта вычисленная высота определяется как контент-область (content-area) элемента и я буду использовать этот термин далее в этой статье. Вы можете думать о контент-области, как об области, для которой применяется свойство background
.
Мы также можем увидеть, что заглавные буквы имеют высоту 68px (680 единиц) и строчные буквы (x-height) — 49px (485 единиц). В результате, 1ex
= 49px и 1em
= 100px, а не 164px (к счастью, em
основан на размере шрифта, а не на вычисленной высоте).
Прежде чем идти дальше, оставлю некоторое пояснение. Когда элемент <p>
отображается на экране, то он может состять из нескольких линий, в соответствии с его шириной. Каждая строка состоит из одного или нескольких inline-элементов (HTML-теги или встроенные imline-элементы для текстового содержимого) и имеет название line-box. Высота line-box основана на высоте его дочерних элементов. Таким образом, браузер вычисляет высоту для каждого inline-элемента, и, следовательно, высоту line-box (от высшей точки своего дочернего элемента к самой нижней точке своего дочернего элемента). В результате line-box — достаточно высок, чтобы содержать все свои дочерние элементы (по умолчанию).
Каждый HTML-элемент на самом деле является неким множеством, состящим из line-boxes. Если вы знаете высоту каждого line-box, Вы знаете высоту элемента.
Если обновить предыдущую страницу HTML код, то получим:
<p>
Good design will be better.
<span class="a">Ba</span>
<span class="b">Ba</span>
<span class="c">Ba</span>
We get to make a consequence.
</p>
Он будет генерировать три line-boxes:
- первый и последний, каждый из которых содержит единственный встроенный элемент (содержание текста)
- второй, содержащий два встроенных элемента и три
<span>
Мы ясно видим, что второй line-box выше остальных, в связи с вычисленной контент-областью его дочерних элементов, один из которых использует шрифт Catamaran.
Трудным в создании line-box является то, что мы не можем видеть и управлять им с помощью CSS. Даже применяя background с псевдоклассом ::first-line
не дает нам ни какой визуальной подсказки о высоте line-box.
line-height: к проблемам и за ее пределами
Ранее я ввел два понятия: контент-область (content-area) и line-box. Если вы читали внимательно, то я сказал, что высота line-box вычисляется в соответствии с его дочерней высотой, я не сообщал дочерним элементам высоту их контент-области. В этом и заключается больше различие.
Хотя это может показаться странным, inline-элемент имеет две разные высоты: высоту content-area и высоту virtual-area (Я изобрел термин virtual-area , обозначая таким образом высоту, невидимую для нас. Этого термина Вы не найдете в спецификации).
- высота content-area определяется показателями шрифта (как показано выше)
- высота virtual-area — это
line-height
, а также это высота, которая используется для вычисления высоты line-box
Это ломает распространенное мнение о том, что line-height
это расстояние между базовыми линиями (baselane). В CSS это не так.
Рассчитанная разность высот между virtual-area и content-area называется ведущей. Половина этой ведущей добавляется к верхней части content-area, другая часть — к нижней. Content-area располагается всегда в центре virtual-area.
На основе его значений, line-height
(virtual-area) может быть больше, меньше или равной content-area. В случае меньшего размера virtual-area, ведущее значение станет отрицательным и line-box стане визуально меньше его дочернего элемента.
Существуют также другие виды inline-элементов:
- inline-элементы (
<img>
,<input>
,<svg>
, etc.) inline-block
и всеinline-*
элементы- inline-элементы, которые используются в качестве форматирования текста (например, во flexbox-элементах)
Для этих inline-элементов, высота вычисляется на основе height
, margin
и border
. Если height
имеет значение auto
, то line-height
используется в content-area и строго равна line-height
.
Итак, вернемся к FontForge. Em-квадрат шрифта Catamaran равен 1000, но мы видим значения ascender/descender:
- Ascent/Descent: ascender — 770 и descender — 230. Используется для символьных рисунков. (табл. “OS/2”)
- показатели Ascent/Descent: ascender — 1100 и descender — 540. Используется для высоты content-area. (табл. “hhea” и табл. “OS/2”)
- показатель Line Gap. Используется дл
line-height: normal
, путем добавления этого значения показателей Ascent/Descent. (табл. “hhea”)
В нашем случае, шрифт Catamaran определяет разрыв в 0 единиц для line gap, поэтому line-height: normal
будет равен content-area, который составляет 1640 единиц, иначе 1.64.
В качестве сравнения, шрифт Arial описывает em-квадрат в 2048 единиц, ascender — 1854, descender — 434 и line gap составляет 67. Это означает, что font-size: 100px
дает content-area — 112px (1117 единиц) и line-height: normal
— 115px (1150 единиц, иначе 1.15). Все эти показатели шрифта устанавливаются разработчиком шрифта.
Становится очевидным, что установка line-height: 1
плохая практика.Я хотел бы напомнить вам, что безразмерное значение относительно для font-size
и virtual-area меньше, чем content-area. Именно это и является источником многих наших проблем.
Уточнения о вычислении line-box:
- для inline-элементов,
padding
иborder
увеличивают область фона, но не высоту content-area (или высоту line-box).Сontent-area — это не всегда то, что ты видишь на экране.margin-top
иmargin-bottom
не имеют ни какого эффекта. - для inline-элементов,
inline-block
и blocksified inline-элементы:padding
,margin
иborder
увеличиваютheight
, а значит увеличивают content-area и высоту line-box
vertical-align: свойство, которое управляет всеми
Я еще не упомянул о свойстве vertical-align
, несмотря на то, что оно важно для вычисления высоты line-box. Можно даже сказать, что vertical-align
играет ведущую роль в форматировании текста.
Значение по умолчанию — baseline
. Помните о показателях шрифта ascender и descender? Эти значения определяют, где базовая линия стоит, а значит определяет и ее соотношение. Т.к. соотношение между ascenders и descenders редко бывает 50:50.
Начнём с кода:
<p>
<span>Ba</span>
<span>Ba</span>
</p>
p {
font-family: Catamaran;
font-size: 100px;
line-height: 200px;
}
Тег <p>
с двумя <span>
унаследовал значения font-family
, font-size
и имеют постоянный line-height
. Базовые линии (baselines) будут соответствовать и высота line-box будет равна их line-height
.
Что делать, если второй элемент имеет меньший font-size
?
span:last-child {
font-size: 50px;
}
Как ни странно, но по умолчанию базовая линия может выровнять выше (!) line-box так, как показано на картинке. Я хотел бы напомнить вам, что высота line-box вычисляется от самой высокой точки своего дочернего элемента к низшей точке своего дочернего элемента.
Это может быть аргументом в качестве использования безразмерного значения line-height
, но иногда необходимо его фиксировать, чтобы создать идеальный вертикальный размер. Если честно, то независимо от того, что вы выберете, вы всегда будете иметь проблемы с inline выравниванием.
Рассмотрим еще один пример. Тег <p>
с line-height: 200px
, содержащий один <span>
, унаследовал значение line-height
<p>
<span>Ba</span>
</p>
p {
line-height: 200px;
}
span {
font-family: Catamaran;
font-size: 100px;
}
Насколько высок line-box? Следует ожидать, что 200px, но это не так. Проблема здесь заключается в том, что <p>
имеет свой собственный font-family
(по умолчанию serif
). Базовые линии тега <p>
и <span>
скорее всего, будут различны, а высота line-box выше, чем ожидалось. Это происходит потому, что браузеры производят вычисления следующим образом: так, как если бы каждый line-box начинался бы с нулевой ширины символа.
Невидим, но заметен.
В итоге, мы столкнулись с той же проблемой, что и в предыдущем примере.
Базовая линия скачен, но что можно сказать на счетvertical-align: middle
? Как пишут в спецификации, middle
“выравнивает вертикальную среднюю точку контейнера с базовой линией родительского контейнера плюс половина x-height родительского блока”. Соотношение базовых линий различны, а также как различно соотношение x-height, следовательно выравнивание middle
не надежно. Как правило, middle
никогда не бывает по “середине”. Этому способствуют много факторов в CSS (x-height, ascender/descender соотношение, и тд.)
В качестве примечания:
vertical-align: top
/bottom
выравнивает по верху и низу line-boxvertical-align: text-top
/text-bottom
выравнивает по верху и низу content-area
Но используйте с осторожностью, это выравнивает virtual-area. Посмотрите это на простом примере, используя vertical-align: top
. Невидимый line-height
может привести к странным, но не удивительным, результатам.
И в заключении стоит отметить, что vertical-align
также принимает числовые значения, которые повышают или понижают поле относительно базовой линии.
CSS превосходен
Мы поговорили о том, как line-height
и vertical-align
работают вместе, но теперь вопрос: можно ли управлять показателями шрифта с помощью CSS? Короткий ответ: нет.
Что делать, если, например, мы хотим, чтобы текст с помощью шрифта Catamaran, где высота заглавной буквы 100px стал выше? Давайте немного займемся математикой.
Сначала мы устанавливим все показатели шрифта в качестве CSS-свойств, а затем вычислим font-size
,чтобы получить высоту заглавной буквы 100px.
p {
/* font metrics */
--font: Catamaran;
--capitalHeight: 0.68;
--descender: 0.54;
--ascender: 1.1;
--linegap: 0;
/* desired font-size for capital height */
--fontSize: 100;
/* apply font-family */
font-family: var(--font);
/* compute font-size to get capital height equal desired font-size */
--computedFontSize: (var(--fontSize) / var(--capitalHeight));
font-size: calc(var(--computedFontSize) * 1px);
}
Довольно просто, не так ли? Но что, если мы хотим, чтобы текст визуально стоял по середине? Так чтобы оставшееся пространство равномерно распределялось по верхней и нижней части буквы «B»? Для достижения этой цели, мы должны вычислить vertical-align
базирующийся на соотношении ascender/descender.
Во-первых, установим line-height: normal
и высоту content-area:
p {
…
--lineheightNormal: (var(--ascender) + var(--descender) + var(--linegap));
--contentArea: (var(--lineheightNormal) * var(--computedFontSize));
}
Затем нам нужно установить:
- расстояние от нижней части буквы к нижнему краю
- расстояние от верхней части буквы к верхнему краю
Вот так:
p {
…
--distanceBottom: (var(--descender));
--distanceTop: (var(--ascender) - var(--capitalHeight));
}
Теперь мы можем вычислить vertical-align
, которая представляет собой разность между расстояниями, умноженное на величину font-size
. (мы должны применить это значение для дочернего элемента inline)
p {
…
--valign: ((var(--distanceBottom) - var(--distanceTop)) * var(--computedFontSize));
}
span {
vertical-align: calc(var(--valign) * -1px);
}
В конце концов, мы устанавливаем желаемый line-height
и вычисляем его при сохранении вертикального выравнивания:
p {
…
/* desired line-height */
--lineheight: 3;
line-height: calc(((var(--lineheight) * var(--fontSize)) - var(--valign)) * 1px);
}
Добавить значок, высота которого соответствует букве «B», теперь легко:
span::before {
content: '';
display: inline-block;
width: calc(1px * var(--fontSize));
height: calc(1px * var(--fontSize));
margin-right: 10px;
background: url('https://cdn.pbrd.co/images/yBAKn5bbv.png');
background-size: cover;
}
Посмотрите результат на JSBin
Обратите внимание, что это демо предназначено только для демонстрационных целей. Почему? На это есть много причин:
- показатели шрифта могут быть не постоянными ¯\(ツ)/¯
- если шрифт не загружен, то резервный шрифт, верноятно, будет иметь различные показатели шрифта и иметь дело с несколькими значениями, а значит — станет не управляемым.
Выводы
- inline форматирование текста действительно трудно понять;
- все inline-элементы имеют 2 высоты:
- контент-области, иначе content-area (данные берутся из показателей шрифта)
- виртуальная область, иначе virtual-area (
line-height
) - ни один из этих 2-х высот не может быть визуализирован.
line-height: normal
основан на показателях шрифтаline-height: normal
может создать virtual-area меньше, чем content-areavertical-align
не очень надёжен- высота line-box вычисляется на основе своих дочерних элементов со свойствами
line-height
иvertical-align
- мы не можем легко получить / установить показатели шрифта при помощью CSS
- есть спецификация, чтобы помочь с вертикальным выравниванием: Line Grid module
Но я до сих пор люблю CSS 🙂
Ресурсы
- Показатели шрифта: FontForge, opentype.js
- Вычисление
line-height: normal
и соотношения в браузере - Ahem, специальный шрифт, чтобы понять, как это работает
- Более детальное описание inline форматирования текста
Перевод статьи «Deep dive CSS: font metrics, line-height and vertical-align»
Комментарии
Сергей
22.02.2024
Классная статья! Молодец!
NikW
10.03.2019
Чтобы легко понять законы по которым живёт текст в вебе, нужно понять базовые принципы типографики и устройства шрифта. После этого описанные в статье CSS-свойства и прочие понятия начинают становятся ясными и логичными, а некоторые шрифты больше никогда не используются — ибо сделаны через задницу и именно поэтому не становятся как надо.
Очень рекомендую всем кто хочет перейти с текстом на «ты» прочесть две книги: Д. Феличи — Типографика, шрифт, вёрстка, дизайн и Р. Брингхерст — Основы стиля в типографике.
css-monster
28.02.2017
Спасибо! Не знал, что так можно делать…