Контейнер обнимает текст
Про странности работы с размером шрифта и высотой строки на вебе уже писали, и лучше моего. Но зря я что ли сам разбирался! Плюс, я принёс практические решения. Так что давайте ещё разок.
Метрики шрифта
Коротко, файл со шрифтом содержит несколько важных циферок. Это метрики шрифта:
- Размер em-квадрата. Это произвольная область, задающая масштаб системы координат для знаков внутри шрифта.
- Положение базовой линии. Базовая линия задаёт начало координат. По договорённости, буквы без нижних выносных элементов «стоят» прямо на базовой линии.
- Высота заглавных букв (cap height). Если автор шрифта не накосячил, это действительно высота заглавных букв без выносных элементов.
- Высота строчных букв (x height) — аналогично, буквально высота буквы „x“.
- Ascender и descender. Показывают, сколько места давать буквам сверху и снизу от базовой линии. В сумме они задают «естественную» высоту строки для шрифта.
Например, у шрифта Source Sans 3 такие метрики:
- Размер em-квадрата: 1000 единиц
- Базовая линия: 200 единиц от нижней стороны квадрата (и, соответственно, 800 единиц от верхней)
- Высота заглавных: 660 единиц
- Высота строчных: 478 единиц
- Ascender: 1024, descender: 400, в сумме 1424 единицы.
— Петька, cap height!
— 660!
— Что 660?
— А что cap height?
Шрифты векторные, так что никаких абсолютных единиц, вроде пикселей, внутри самого шрифта нет. Есть только сетка единиц, задаваемая em-квадратом и базовой линией.
Что мы имеем в виду, когда говорим font-size
Указывая в CSS размер шрифта в пикселях (например, font-size: 100px
), мы на самом деле задаём абсолютный размер em-квадрата. Собственно, поэтому текущий размер шрифта в CSS — это 1em
.
Немного арифметики. У Source Sans 3 em-квадрат на 1000 единиц, так что при font-size: 100px
одна единица в координатах шрифта в реальности занимает на экране 0,1 пиксель. Заглавные буквы высотой 660 единиц имеют реальную высоту 66 пикселей. А span
с таким размером шрифта имеет высоту 142,4 пикселя (с округлением до целого физического пикселя).
Em-квадрат — относительно произвольная область, не видимая невооружённым взглядом. В зависимости от того, как она задана, разные шрифты будут выглядеть крупнее или мельче при том же размере шрифта.
При сочетании разных шрифтов на одной странице такая непредсказуемость становится проблемой.
Делаем размер шрифта предсказуемым
Что считать настоящим визуальным размером шрифта: высоту строчных (x height) или высоту заглавных (cap height)? В интерфейсе, если надо выровнять текст с иконкой, я обычно ориентируюсь на заглавные; а в журнальной вёрстке сочетают шрифты с одинаковой высотой строчных.
Для себя я выбрал высоту заглавных как визуальную метрику размера шрифта, а если вам это не подходит — просто замените везде дальше cap height на x height.
Зная размер em-квадрата и cap height, посчитаем, какой указывать font-size
, чтобы получить желаемую высоту заглавных:
размер шрифта = желаемая высота заглавных ÷ высота заглавных в em
высота заглавных в em = высота заглавных ÷ размер em-квардрата
Например, хотим иметь одну высоту заглавных в 72 пикселя для трёх разных шрифтов:
Source Sans 3: 72px ÷ (660 / 1000) ≈ 109px
Crimson Pro: 72px ÷ (587 / 1024) ≈ 126px
Roboto: 72px ÷ (1456 / 2048) ≈ 101px.
На практике, чтобы не считать руками каждый раз, заведём несколько вспомогательных стилей:
:root { font-family: 'Source Sans 3'; --cap-height-em: 0.66; /* = 660 / 1000 */ } /* Typography helper */ .tyh { font-size: calc(var(--cap-height) / var(--cap-height-em)); }
Используя наш класс-хелпер, мы теперь можем где угодно задавать размер шрифта, ориентируясь на высоту заглавных:
<p class="tyh" style="--cap-height: 72px"> Hello, world! </p>
Теперь про высоту строки
В метриках неявно зашита «естественная» высота строки для этого шрифта, равная сумме ascender + descender. Указывая в CSS явный line-height
, мы просим браузер докинуть отступов к этой высоте. Эти дополнительные отступы, которые называются leading — читается [лэдинг], — распределяются поровну сверху и снизу:
Вертикальное положение букв в этой области зависит от отношения ascender к descender. Ещё немного арифметики: возьмём Source Sans 3 в блоке с font-size: 100px
и line-height: 150px
. Естественная высота строки при таком размере шрифта равна 142,4 пикселя (округлим до 142). Значит, браузер добавит к блоку 4 дополнительных пикселя сверху и снизу.
При этом расстояние от нижнего края блока до базовой линии составит
descender + ½ leading =
40 + 4 = 44px,
а от верхнего края до верхнего края заглавных —
ascender + ½ leading − cap height =
102,4 + 4 − 66 ≈ 40px.
Получается, заглавные буквы оказываются не строго в центре области, выделенной под строку. Это тоже головная боль, например, в интерфейсах, когда иконка и текст в блоках равной высоты оказываются смещены относительно друг друга.
Контейнер обнимает текст
В идеальном мире, свойство line-height
устанавливает только расстояние между базовыми линиями строк текста, не добавляя дополнительных отступов над первой и под последней строкой.
В ещё более идеальном мире вертикальное положение текста в блоке вообще не зависит от таких произвольных метрик, как ascender и descender. Для взаимного выравнивания блоков с текстом удобнее всего, если верхний край блока прижимается к верхнему краю заглавных, а нижний — к базовой линии.
Научим наш класс .tyh
производить все необходимые расчёты:
:root { font-family: 'Source Sans 3'; --cap-height-em: 0.66; /* = 660 / 1000 */ --ascender-em: 1.024; /* = 1024 / 1000 */ --descender-em: 0.4; /* = 400 / 1000 */ } /* Typography helper */ .tyh { /* Коэффициент для перевода em в абсолютные величины, а заодно и значение font-size: */ --units-per-em: calc(var(--cap-height) / var(--cap-height-em)); --half-leading: calc(( var(--line-spacing) - (var(--ascender-em) + var(--descender-em)) * var(--units-per-em) ) / 2); --trim-top: calc(-1 * ( var(--half-leading) + var(--ascender-em) * var(--units-per-em) - var(--cap-height) )); --trim-bottom: calc(-1 * ( var(--half-leading) + var(--descender-em) * var(--units-per-em) )); font-size: var(--units-per-em); line-height: var(--line-spacing); }
Осталось добавить блоку отрицательные поля, откусывающие --trim-top
сверху и --trim-bottom
снизу. Чтобы избежать проблем с margin collapsing, добавим их не самому блоку, а псевдоэлементам в начале и конце блока:
.th::before, .th::after { content: ""; display: table; } .th::before { margin-bottom: var(--trim-top); } .th::after { margin-top: var(--trim-bottom); }
<p class="tyh" style="--cap-height: 72px; --line-spacing: 100px;"> The quick brown fox jumps </p>
А вот и демка.
Зачем всё это было, и что делать дальше
Используя класс .tyh
, гораздо проще подгонять текстовые блоки друг к другу, к иллюстрациям и к иконкам. Отдельно замечу, что заодно стало куда легче следить за приводностью вёрстки.
При этом я не считаю, что стоит тащить такой CSS везде. Во-первых, вам придётся объяснить своим коллегам всё то, что я попытался объяснить в этой статье, а это уже не тривиальная задача. Во-вторых, кому-то придётся следить за актуальностью захардкоженных метрик в коде, либо придумывать пайплайн, чтобы автоматизировать их генерацию. В-третьих, возможно, такой перфекционизм просто никому не нужен.
Но если вы всё же решите заморочиться, метрики шрифта можно подсмотреть в FontForge (полезнейшее приложение с ужасным, ужасным интерфейсом), либо достать программно библиотеками вроде fontkit.
Наконец, рекомендую прочитать прекрасные статьи из следующего раздела. Там особенно интересно, откуда взялись все эти странные термины, вроде em square и leading.
Литература
Три статьи, описывающие предметную область лучше, чем я:
Font size is useless; let’s fix it
Deep dive CSS: font metrics, line-height and vertical-align
Getting to the bottom of line height in Figma
Основное вдохновение для класса .tyh
— библиотека Capsize.