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

Почему высоту строки не определяет самый высокий фрагмент?

Представьте заголовок страницы с крупной подписью Label A рядом с мелкой Label B. У обоих фрагментов задано line-height: 40px, однако высота содержащей их строки превышает 40 CSS-пикселей. Это объясняется тем, как при встроенной раскладке выравниваются блоки перед вычислением охватывающей их строки.

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

Базовая линия служит ориентиром для выравнивания текста. Она не проходит посередине вклада его line-height. Одинаковая высота строки для текста разного размера не означает, что у обоих фрагментов одинаковое расстояние над базовой линией и под ней. Начальное значение vertical-align — baseline. MDN: vertical-align

Различайте три величины: метрики восходящих и нисходящих элементов шрифта, видимые границы конкретных букв и пространство, занимаемое при раскладке строки. Цветной фон или прямоугольник выделения фрагмента нельзя считать прямым измерением охватывающего его блока строки: CSS различает геометрию отрисовки встроенных элементов и расчёт высоты строки. W3C: интерлиньяж и половинный интерлиньяж

Как из двух вкладов по 40px получается строка высотой 48,4px?

Для обычного текста, выровненного по базовой линии и имеющего явную высоту строки, полезно выполнить такой расчёт: вычесть сумму восходящей и нисходящей метрик шрифта из высоты строки, а затем поровну распределить остаток над и под ними. Это дополнительное пространство называется интерлиньяжем, а каждая его половина — половинным интерлиньяжем. В рабочем черновике CSS Inline Layout Level 3 описан этот расчёт и допускается отрицательный интерлиньяж. Статус черновика не гарантирует поддержку браузерами всех новых свойств, которые он содержит. W3C: расчёт границ раскладки

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

Вклад Восходящая метрика шрифта Нисходящая метрика шрифта Высота строки Половинный интерлиньяж Над базовой линией Под базовой линией
Крупный фрагмент 32px 8px 40px 0px 32px 8px
Малый фрагмент 9.6px 2.4px 40px 14px 23.6px 16.4px
Родительская распорка 9.6px 2.4px 40px 14px 23.6px 16.4px

В этом примере с выравниванием по базовой линии независимо возьмите наибольшую величину с каждой стороны: max(32, 23.6, 23.6) + max(8, 16.4, 16.4) = 48.4px. Вклад каждого элемента равен 40px, но верхнюю границу задаёт крупный фрагмент, а нижнюю — малый фрагмент и родитель. Этот пример расчёта основан на описанном алгоритме раскладки и не является прогнозом измерения в браузере. W3C: модель встроенной раскладки

Поэтому при диагностике нужно спросить: какой участник задаёт каждую границу? Уменьшение высоты строки малого фрагмента может убрать его вклад в нижнюю границу, но такой же вклад от родительской распорки останется.

Как воспроизвести и проверить это на Mac?

В этой инструкции используйте Chrome на Mac. Откройте новую вкладку, перейдите на about:blank, затем нажмите Command + Option + J, чтобы открыть Console. Chrome описывает это сочетание клавиш как способ запускать JavaScript. Chrome DevTools: обзор

Выполните в этой пустой вкладке всю приведённую ниже программу. Она создаёт три искусственные строки с одинаковыми подписями, выводит результаты измерений и использует теневой корень, чтобы объединить стили демонстрации. Установка пакетов и внешние библиотеки не требуются. Повторный запуск заменит предыдущую демонстрацию.

(async () => {
  document.getElementById('line-box-demo')?.remove();
  const host = document.createElement('div');
  host.id = 'line-box-demo';
  document.body.append(host);
  const root = host.attachShadow({ mode: 'open' });
  const style = document.createElement('style');
  style.textContent = `
    :host { display: block; padding: 24px; color: #222; background: #fff; }
    .label { margin: 20px 0 6px; font: 14px Arial, sans-serif; }
    .row { font: 12px/40px Arial, sans-serif; white-space: nowrap;
      margin: 0; padding: 0; border: 0; background: #e8eef4; }
    .large { font-size: 40px; line-height: 40px; }
    .small { font-size: 12px; line-height: 40px; }
    .trim-span .small, .trim-both .small { line-height: 12px; }
    .trim-both { font-size: 40px; }
  `;
  root.append(style);
  const cases = [
    ['', 'Shared 40px line-height'],
    ['trim-span', 'Smaller span only'],
    ['trim-both', 'Parent and smaller span']
  ];
  const rows = cases.map(([className, label]) => {
    const caption = document.createElement('p');
    caption.className = 'label';
    caption.textContent = label;
    const row = document.createElement('div');
    row.className = `row ${className}`;
    for (const [size, text] of [['large', 'Label A'], ['small', ' Label B']]) {
      const span = document.createElement('span');
      span.className = size;
      span.textContent = text;
      row.append(span);
    }
    root.append(caption, row);
    return { label, row };
  });
  await document.fonts.ready;
  console.table(rows.map(({ label, row }) => ({
    case: label,
    parentLineHeight: getComputedStyle(row).lineHeight,
    largeLineHeight: getComputedStyle(row.children[0]).lineHeight,
    smallLineHeight: getComputedStyle(row.children[1]).lineHeight,
    rowHeight: row.getBoundingClientRect().height
  })));
})();

Программа ждёт document.fonts.ready: этот объект разрешается после загрузки шрифтов документа и связанной с ней работы раскладки. Если вы позже добавите другой шрифт или измените содержимое, выполните измерение повторно после завершения этих изменений. MDN: FontFaceSet.ready

Сравните три строки: условия в них меняются последовательно. В первой высота строки у обоих фрагментов и родительского элемента равна 40px. Во второй высота строки уменьшена до 12px только у малого фрагмента. В третьей размер шрифта родительского элемента также изменён на 40px, чтобы его распорка соответствовала размеру шрифта и высоте строки крупного текста. Если вторая строка остаётся высокой, а третья становится ниже, это сравнение указывает на вклад родительского элемента.

Ориентируйтесь на результаты измерений в браузере, а не на иллюстративное значение 48,4px. Программа измеряет каждую строку с помощью getBoundingClientRect(), который возвращает ограничивающий прямоугольник элемента, включая внутренние поля и границы. В данном случае у строк их нет, каждая содержит одну строку текста без переноса и имеет автоматическую высоту, поэтому измеренная высота служит полезным приближением к высоте блока строки. Измерение фрагмента с помощью того же API вернёт прямоугольник этого фрагмента, но напрямую не покажет ограничивающий блок строки. MDN: getBoundingClientRect()

Что проверить в существующей раскладке?

Щёлкните нужный текст, удерживая Control, и выберите Inspect. Во вкладке Elements проверьте оба фрагмента и содержащий их блок. Во вкладке Styles проследите, откуда берутся объявления, а во вкладке Computed посмотрите применённые значения; при необходимости включите Show All, чтобы проверить унаследованные свойства. Chrome описывает эти представления и фильтры свойств. Chrome DevTools: справочник по функциям CSS

Запишите значения font-family, font-size, line-height, display и vertical-align для каждого участника. Начните с родительского элемента, а не только с двух фрагментов. Затем выполните следующие действия, чтобы определить причину:

  1. Уточните модель раскладки. Определите, участвуют ли дочерние элементы в обычном потоке встроенных элементов или являются элементами flex- или grid-контейнера.
  2. Убедитесь, что строка одна. Используйте широкую тестовую область и короткие искусственные подписи, чтобы перенос текста не влиял на измерения.
  3. Проверьте выравнивание. Прежде чем применять упрощённый расчёт выше, временно уберите смещения от базовой линии и нестандартное вертикальное выравнивание.
  4. Изменяйте по одному вкладу. Уменьшите высоту строки малого фрагмента и измерьте результат, затем отдельно проверьте вклад шрифта и высоты строки родительского элемента.
  5. Верните реалистичные условия. После определения причины повторно проверьте результат с нужным шрифтом, доступной шириной и текстом.

Также проверьте наследование. Единичное значение line-height: 1.2 передаёт множитель, который каждый потомок применяет к собственному размеру шрифта. Длина вроде 40px передаёт фиксированную величину, если её не переопределить; проценты вычисляются относительно размера шрифта элемента, в котором они объявлены. Из-за этих вариантов у мелкого текста может быть неожиданно просторный интерлиньяж. Значение normal зависит от шрифта и браузера, поэтому на него нельзя полагаться как на фиксированную числовую величину. MDN: значения line-height и наследование

Какую корректировку выбрать?

Выбирайте корректировку с учётом назначения двух подписей. К компактному заголовку из отдельных подписей предъявляются иные требования, чем к словам внутри абзаца с переносом строк.

Желаемый результат Что попробовать Что проверить
Сохранить общую базовую линию текста Задать малому фрагменту подходящую высоту строки и согласовать метрики шрифта родительского элемента с основным текстом Родительская распорка больше не задаёт лишнюю нижнюю границу
Выровнять верхние границы отдельных подписей Проверить vertical-align: top для обоих встроенных фрагментов Изменённое положение базовых линий соответствует дизайну
Расположить отдельные подписи по центру строки Использовать flex-контейнер со свойством align-items: center Поведение на узком экране и перенос элементов подходят раскладке
Сохранить непрерывный поток абзаца Оставить встроенный текст и задать продуманные наследуемые значения высоты строки Все получившиеся строки вмещают текст разного размера

vertical-align: top выравнивает элемент и его потомков по верхнему краю строки. У middle другое определение: свойство выравнивает середину элемента относительно базовой линии родителя плюс половина высоты его строчной буквы x. Поэтому оно не означает геометрическое центрирование по всей строке, а изменение выравнивания не гарантирует определённую итоговую высоту. MDN: значения vertical-align

Для двух независимых подписей flexbox позволяет явно задать выравнивание по поперечной оси с помощью align-items. Центрирование и выравнивание по базовой линии отражают разные дизайнерские решения. Переход на flexbox меняет контекст раскладки, поэтому используйте его, когда подписи являются отдельными элементами, и проверьте результат на узких экранах. MDN: align-items

Меньшая высота строки также может привести к тому, что текст будет отрисовываться за пределами своего вклада в раскладку. После уменьшения интервалов проверьте соседние строки: компактный измеренный блок сам по себе не гарантирует, что текст помещается без наложения. W3C: интерлиньяж и половинный интерлиньяж

Могут ли измерения canvas заменить раскладку текста в браузере?

Измерения canvas отвечают на смежный вопрос. WHATWG определяет measureText() и различает метрики восходящих и нисходящих элементов шрифта и фактические границы конкретного текста. Эти измерения полезны при рисовании на canvas, но измерение границ видимых символов не учитывает родительскую распорку в DOM, выравнивание встроенных элементов и их вклад в высоту строки. Не используйте его отдельно для предсказания высоты этой строки. WHATWG: TextMetrics

При экспериментах сохраняйте границы формирования текста. CSS требует продолжать формирование текста между границами встроенных элементов, если форматирование фактически не меняется; при некоторых границах, включая нестандартное вертикальное выравнивание, формирование должно прерываться. Не оборачивайте каждый символ отдельно ради измерения: разделение графемных кластеров или лигатур может привести к непоследовательным результатам. Сохраняйте подписи целиком или в виде осмысленных фрагментов текста. W3C: обработка текста и формирование на границах элементов

Чтобы устранить лишнюю высоту, определите, какой участник задаёт верхнюю границу и какой — нижнюю, включая родительский элемент. Измените их вклад в высоту строки или выберите модель выравнивания, соответствующую назначению подписей, а затем повторно измерьте всю строку с нужным текстом и шрифтом.

Источники