Статьи

Незвідані глибини CSS: метрики шрифту, line-height і vertical-align - CSS-LIVE

  1. Поговоримо спочатку про font-size
  2. line-height: до підводних каменів і далі
  3. vertical-align: одна властивість, щоб управляти всіма
  4. «CSS is awesome»
  5. У сухому залишку
  6. Корисні посилання

Переклад статті Deep dive CSS: font metrics, line-height and vertical-align з сайту iamvdo.me , Опубліковано на css-live.ru з дозволу автора - Венсана де Олівейри .

Line-height і vertical-align - прості CSS-властивості. Настільки прості, що більшість з нас упевнені, що розуміють, як вони працюють і як ними користуватися. Але це не так. На ділі вони складні, може бути, найскладніші, тому що у них провідна роль у створенні однієї з найменш відомих особливостей CSS: сатиричного (інлайнового) контексту форматування.

Наприклад, line-height можна задати у вигляді довжини або безрозмірного значення 1 , Але за замовчуванням у нього значення 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> з 3 <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-size, різна висота в результаті
Різні гарнітури, однаковий font-size, різна висота в результаті

Навіть якби ми знали цю особливість, чому font-size: 100px робить елементи висотою 100px? Я виміряв ці значення: Helvetica - 115px, Gruppo - 97px і Catamaran - 164px.

Висота елементів з font-size: 100px коливається від 97px до 164px
Висота елементів з font-size: 100px коливається від 97px до 164px

Хоча на перший погляд це дивно, це цілком очікувано. Причина в самому шрифті. Ось як це працює:

  • шрифт задає свої одиниці виміру em-квадрата (Або UPM, units per em - одиниць на кегль), якийсь контейнер, в якому будуть малюватися все символи. В цьому контейнері все вимірюється в відносних одиницях і зазвичай він приймається за 1000 одиниць. Але буває і 1024, і 2048, і скільки завгодно.
  • на базі цих відносних одиниць задаються метрики шрифту: висота верхніх виносних елементів (ascender), нижніх виносних (descender), вживання великої літери (capital height), малих літер (x-height) і т.п. Зауважте, що деякі значення можуть виступати за рамки em-квадрата.
  • в браузері ці відносні одиниці масштабируются до заданого font-size.

Візьмемо шрифт Catamaran і відкриємо його в FontForge , Щоб побачити метрики:

  • em-квадрат прийнятий за 1000 одиниць
  • висота верхніх виносних - 1100, а нижніх - 540. Судячи з кількох тестів, браузери використовують значення HHead Ascent / Descent на Mac OS і Win Ascent / Descent на Windows (і ці значення можуть відрізнятися!). Також видно, що висота заголовних букв (Capital Height) дорівнює 680, а висота рядкових (X height) - 485.

Значення метрик шрифту в FontForge
Значення метрик шрифту в FontForge

Це означає, що шрифт Catamaran використають 1100 + 540 одиниць з em-квадрата в 1000 одиниць, що при font-size: 100px дає висоту 164px. Ця обчислена висота визначає область вмісту елемента, так я і буду називати її всю статтю. Можете уявляти область вмісту як ту область, до якої застосовується властивість background 2 .

Також легко передбачити, що висота заголовних букв буде 68px (680 одиниць), а малих (x-висота) - 49px (485 одиниць). Як результат, 1ex = 49px, а 1em = 100px, а не 164px (на щастя, em відраховується від font-size, а не від обчисленої висоти)

Шрифт Catamaran: одиниці на em (UPM) і їх еквіваленти в пікселях при font-size: 100px
Шрифт Catamaran: одиниці на em (UPM) і їх еквіваленти в пікселях при font-size: 100px

Перш ніж далі лізти вглиб, коротко розглянемо основні поняття, з якими доведеться мати справу. Коли елемент <p> відображається на екрані, він може складатися з декількох рядків, що займають доступну ширину. Кожен рядок набирається з декількох елементів рядка (HTML-тегів анонімних елементів рядка для текстового вмісту) і називається контейнером рядка (line-box). Висота контейнера рядка визначається висотами його нащадків. Таким чином, браузер розраховує висоту кожного елемента рядка, а по ній - висоту контейнера рядка (від найвищої до найнижчої точки її нащадків). В результаті висоти контейнера рядка завжди вистачає, щоб вмістити всіх його нащадків (з побудови).

Кожен HTML-елемент насправді являє собою «стопку» контейнерів рядки. Якщо ви знаєте висоту кожного контейнера рядка, ви знаєте і висоту елемента.

Якщо ми змінимо попередній 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>

Він згенерує 3 контейнера рядка:

  • в першому і останньому - тільки по одному анонімному елементу рядка (текст)
  • у другому - два анонімних елемента рядки і 3 <span> -а

Елемент <p> (чорна рамка) складається з контейнерів рядків (білі рамки), що складаються з малих елементів (суцільні рамки) і анонімних елементів рядка (пунктирні рамки)
Елемент <p> (чорна рамка) складається з контейнерів рядків (білі рамки), що складаються з малих елементів (суцільні рамки) і анонімних елементів рядка (пунктирні рамки)

Добре видно, що другий контейнер рядки вище за інших, через обчисленої області вмісту його нащадків, а точніше - того, що зі шрифтом Catamaran.

Найскладніше у створенні контейнера рядка то, що ми не можемо ні толком це побачити, ні управляти цим з CSS. Навіть завдання фону для :: first-line не допомагає уявити висоту першого контейнера рядка наочно.

line-height: до підводних каменів і далі

До сих пір я ввів два поняття: область вмісту і контейнер рядки. Якщо ви читали уважно, я сказав, що висота контейнера рядка розраховується по висоті його нащадків, я не говорив «висоті області вмісту нащадків». І в цьому велика різниця.

Хоч це може прозвучати дивно, у елемента рядка є дві різні висоти: висота області вмісту і висота віртуальної області (я придумав термін «віртуальна область», тому що ця висота нам не видно, але в специфікації ви ніде цього терміна не знайдете).

  • висота області вмісту визначається метриками шрифту (як ми бачили раніше)
  • висота віртуальної області - це line-height, і саме ця висота використовується при розрахунку висоти контейнера рядка

У елементів рядка є дві різні висоти
У елементів рядка є дві різні висоти

Крім іншого, це спростовує популярне уявлення, що line-height - це відстань між базовими лініями шрифту. В CSS це не так 3 .

В CSS line-height - НЕ відстань між базовими лініями
В CSS line-height - НЕ відстань між базовими лініями

Обчислена різниця висот між віртуальною областю та областю вмісту називається «leading», або інтерліньяж (прим. Перекл .: в деяких російськомовних джерелах «інтерліньяж» називають саму «висоту віртуальної області» в термінах Венсана, що часто призводить до плутанини. Цікаво, що у французької версії статті Венсан використовує теж англомовний термін «leading»). Половина інтерліньяжу додається зверху від області вмісту, друга половина знизу від неї. Таким чином, область вмісту завжди буде посередині віртуальної області.

Залежно від обчисленого значення, line-height (віртуальна область) може дорівнювати, вище або нижче області вмісту. Якщо віртуальна область нижче, то інтерліньяж негативний і контейнер рядки візуально виявляється нижче власних нащадків.

Є й інші види елементів рядка:

  • заміщаються елементи рядка (<img>, <input>, <svg> і т.п.)
  • елементи типу inline-block та інших inline- *
  • рядкові елементи, які беруть участь в особливих контекстах форматування (наприклад, у флекс-контейнері все флекс-елементи «облочняются»)

Для цих особливих малих елементів висота розраховується за їх властивостями height, margin and border. Якщо у height значення auto, то використовується line-height, і висота області вмісту строго дорівнює line-height.

У заміщаються малих елементів, елементів типу inline-block / inline- * і «облочненних» малих елементів висота області вмісту дорівнює їх висоті, або line-height
У заміщаються малих елементів, елементів типу inline-block / inline- * і «облочненних» малих елементів висота області вмісту дорівнює їх висоті, або line-height.

Як би там не було, ми до сих пір не вирішили проблему, чому ж дорівнює значення normal у line-height? І відповідь, як і при розрахунку висоти області вмісту, треба шукати в метриках шрифту.

Повернемося в FontForge. Розмір em-квадрата для шрифту Catamaran дорівнює 1000, але ми бачимо багато інших значень для верхніх / нижніх виносних і не тільки:

  • загальні значення Ascent / Descent: висота верхньої частини дорівнює 770, а нижній - 230. Використовується для відтворення символів (табличка «OS / 2»).
  • метрики Ascent / Descent 1100 вгору і 540 вниз. Використовується для висоти області вмісту (таблички «hhea» і «OS / 2»)
  • метрика Line Gap ( «міжрядковий зазор»). Використовується для line-height: normal, дане значення додається до метрик Ascent / Descent (табличка «hhea»)

У нашому випадку шрифт Catamaran визначає міжрядковий зазор в 0 одиниць, так що line-height: normal буде дорівнює висоті області вмісту, тобто 1 640 одиниць або 1.64.

Для порівняння, в шрифті Arial em-квадрат узятий за 2048 одиниць, висота верхньої частини - 1854 нижній частині - 434, а міжрядковий зазор - 67. Це означає, що при font-size: 100px висота області вмісту буде 112px (2288/2048 ≈1,117), а line-height: normal - 115 px (2355 / 2048≈1.15). Всі ці метрики специфічні для кожного шрифту і задані дизайнером-шрифтовика, який його створив.

Стає очевидно, що вказувати line-height: 1 - поганий підхід. Нагадую, що безрозмірні значення відраховуються від font-size, а не від області вмісту, і чимало проблем виникає від того, що віртуальна область виявляється менше області вмісту.

Через line-height: 1 контейнер рядки може стати нижче, ніж область вмісту
Через line-height: 1 контейнер рядки може стати нижче, ніж область вмісту

Але не тільки line-height: 1. Наприклад, з 1117 шрифтів, встановлених на моєму комп'ютері (так, я встановив все шрифти з Google Web Fonts) у 1059, це близько 95%, обчислена line-height більше 1. Взагалі обчислена line- height у них коливається від 0.618 до 3.378. Ви не привиділося, 3.378!

Трохи подробиць про розрахунок контейнера рядка:

  • для малих елементів padding і border збільшують область фону, але не область вмісту (і не висоту контейнера рядка). Так що не завжди область вмісту - те, що видно на екрані. А margin-top і margin-bottom ні на що не впливають.
  • для заміщаються малих елементів, елементів типу inline-block і «облочненних» малих елементів: padding, margin і border збільшують height, а значить, і висоту області вмісту і контейнера рядка.

vertical-align: одна властивість, щоб управляти всіма

Я ще не згадував властивість vertical-align, хоча воно істотно впливає на розрахунок висоти контейнера рядка. Ми навіть можемо сказати, що vertical-align може відігравати провідну роль в рядковому контексті форматування.

За замовчуванням у нього значення baseline. Пам'ятайте метрики шрифту «ascender» і «descender» (верхня і нижня частини)? Вони визначають, де знаходиться базова лінія, тобто в якій пропорції вона ділить символ на верхню і нижню частини (як ватерлінія корабля ділить його на надводну і підводну частини - прим. перекл.). Оскільки пропорція між верхньою і нижньою частина рідко буває 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. Базові лінії збігаються і висота контейнера рядка дорівнює їх line-height.

Той же шрифт, та ж базова лінія, на вигляд все в порядку
Той же шрифт, та ж базова лінія, на вигляд все в порядку

Що, якщо у другого елементу буде менший font-size?

span: last-child {font-size: 50px; }

Як би дивно це не звучало, і з-за вирівнювання по базовій лінії (за замовчуванням) контейнер рядки може стати вище (!), Як показано на малюнку внизу. Нагадую, що висота контейнера рядка розраховується від найвищої до найнижчої точки його нащадків.

Менший дочірній елемент може привести до більшої висоті контейнера рядка
Менший дочірній елемент може привести до більшої висоті контейнера рядка

Це може бути аргументом на користь безрозмірних значень line-height , Але іноді для створення ідеального вертикального ритму потрібні фіксовані. Сказати чесно, що б ви не вибрали, з вирівнюванням в рядку завжди будуть неприємності.

Ось ще один приклад. Тег <p> з line-height: 200px, а в ньому єдиний <span>, що успадковує його line-height

<P> <span> Ba </ span> </ p> p {line-height: 200px; } Span {font-family: Catamaran; font-size: 100px; }

Якої висоти буде контейнер рядки? Можна подумати, що 200px, але не тут-то було. Проблема в тому, що у цього <p> своє, інше значення font-family (за замовчуванням воно serif). Положення базової лінії у <p> і у <span> напевно різний, тому висота контейнера рядка більше, ніж очікувалося. Це тому, що браузери вважають так, як ніби кожен контейнер рядки починається з символу нульовий ширини, який специфікація називає терміном «strut».

Символ невидимий, але вплив помітне

Разом, перед нами та ж проблема, що й у попередньому випадку з сусідніми елементами.

Кожен нащадок вирівнюється так, як ніби його контейнер рядки починається з символу нульовий ширини
Кожен нащадок вирівнюється так, як ніби його контейнер рядки починається з символу нульовий ширини

Вирівнювання по базовій лінії пролітає, але, може бути, vertical-align: middle нас врятує? Як свідчить специфікація, значення middle «вирівнює вертикальну середину боксу з базовою лінією батьківського боксу плюс половина батьківської x-висоти». Положення базових ліній буває різний, x-висота - теж, так що і на middle покластися не можна. Найгірше те, що в більшості сценаріїв middle ніколи не буває по-справжньому «посередині». Занадто багато всього впливає і це не можна прописати в CSS (x-висота, відносне положення базової лінії і т.п.)

Зазначу, що є ще 4 значення, корисні в деяких випадках:

  • vertical-align: top / bottom вирівнюють по верху або низу контейнера рядка
  • vertical-align: text-top / text-bottom вирівнюють по верху або низу області вмісту

Vertical-align: top, bottom, text-top і text-bottom
Vertical-align: top, bottom, text-top і text-bottom

Але будьте обережні: у всіх випадках воно вирівнює віртуальну область, висота якої невидима. Погляньте на цей простий приклад з vertical-align: top. Невидима line-height може давати дивні, хоча і передбачувані, результати.

vertical-align може давати на перший погляд дивний результат, хоча, якщо наочно уявити line-height, все очікувано
vertical-align може давати на перший погляд дивний результат, хоча, якщо наочно уявити line-height, все очікувано

Нарешті, vertical-align приймає також числові значення, які піднімають або опускають бокс щодо базової лінії. Цей останній варіант часто може виручити.

«CSS is awesome»

Ми поговорили про взаємодію line-height і vertical-align, але питання-то ось у чому: чи можна керувати метриками шрифту з CSS? Коротка відповідь: ні. Хоча я дуже на це сподівався. Як би там не було, думаю, пора нам трохи розім'ятися. Метрики шрифтів - константи, щось же у нас в будь-якому випадку вийде.

Що якщо, наприклад, нам потрібен текст шрифтом Catamaran, в якому великими літерами будуть рівно 100px висотою? На вигляд можна здійснити: давайте трохи порахуємо.

Насамперед зазначимо все метрики шрифту у визначених користувачем властивостей CSS 4 , Потім розрахуємо, при якому font-size заголовні букви будуть висотою 100px.

p {/ * метрики шрифту * / --font: Catamaran; --capitalHeight: 0.68; --descender: 0.54; --ascender: 1.1; --linegap: 0; / * Потрібна висота заголовних букв * / --fontSize: 100; / * Apply font-family * / font-family: var (- font); / * Розраховуємо font-size для отримання потрібної висоти заголовних букв * / --computedFontSize: (var (- fontSize) / var (- capitalHeight)); font-size: calc (var (- computedFontSize) * 1px); }

Тепер великі літери висотою 100px
Тепер великі літери висотою 100px

Досить очевидно, правда? Але що якщо ми хочемо вертикально відцентрувати текст, щоб вільне місце порівну розподілялося зверху і знизу від букви «B»? Щоб домогтися цього, доведеться розрахувати vertical-align на основі положення базової лінії (пропорції верхньої / нижньої частин).

Спочатку розрахуємо величину line-height: normal і висоту області вмісту:

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 (нам треба буде застосувати це значення до строчному дочірньому елементу).

p {... --valign: ((var (- distanceBottom) - var (- distanceTop)) * var (- computedFontSize)); } Span {vertical-align: calc (var (- valign) * -1px); }

І в кінці задамо бажану висоту рядка і обчислимо відповідний line-height так, щоб вертикальне вирівнювання збереглося:

p {... / * бажана висота рядка * / --lineheight: 3; lineheight: calc (((var (- lineheight) * var (- fontSize)) - var (- valign)) * 1px); }

Результати для різних line-height
Результати для різних line-height. Текст завжди посередині.

Тепер легко додати іконку однієї висоти з буквою «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; }

Іконка і буква «B» рівні по висоті
Іконка і буква «B» рівні по висоті

Подивитися результат в JSBin

Врахуйте, що цей тест лише для демонстраційних цілей. Покладатися на це не можна. Причин багато:

  • метрики шрифтів-то постійні, але ось браузерні обчислення - не дуже ¯⁠ \ (ツ) / ⁠¯
  • якщо шрифт не завантажився, у резервного шрифту швидше за все інші метрики, а працювати з декількома різними значеннями швидко стане практично нереально.

У сухому залишку

Що ми дізналися:

  • зрозуміти рядковий (інлайновий) контекст форматування нелегко
  • у всіх малих елементів 2 висоти
    • область вмісту (заснована на метриках шрифту)
    • віртуальна область (line-height)
    • ні ту, ні іншу висоту не можна однозначно візуалізувати (якщо ви розробляєте браузерні відладчик і готові взятися за це - було б чудово!)
  • line-height: normal грунтується на метриках шрифту
  • при line-height: n віртуальна область може стати менше, ніж область вмісту
  • на vertical-align можна особливо покладатися
  • висота контейнера рядка розраховується на основі властивостей line-height і vertical-align його нащадків
  • не можна просто взяти і отримати / встановити метрики шрифту з CSS
  • є в планах специфікація на цю тему, щоб допомогти з вертикальним вирівнюванням: модуль рядкової сітки ( Line Grid )

Але я як і раніше люблю CSS :)

Корисні посилання

  1. неважливо, що ви виберете, тут мова не про це
  2. це не зовсім так
  3. в інших редакторах це може бути і відстань між базовими лініями. В Ворді, в Фотошопі це так. Головна відмінність в тому, що в CSS воно впливає і на перший рядок
  4. можна використовувати і змінні з препроцесорів, не в призначених для користувача властивості суть

PS Це теж может буті цікаво:

Прекрасно, але що значить «нормально»?
А vertical-align, як line-height впливає на нього?
Як би там не було, ми до сих пір не вирішили проблему, чому ж дорівнює значення normal у line-height?
Пам'ятайте метрики шрифту «ascender» і «descender» (верхня і нижня частини)?
Що якщо, наприклад, нам потрібен текст шрифтом Catamaran, в якому великими літерами будуть рівно 100px висотою?
Але що якщо ми хочемо вертикально відцентрувати текст, щоб вільне місце порівну розподілялося зверху і знизу від букви «B»?

Новости