Две плавные шкалы вместо корня 10px
04 Ноя 2025
Выпустил typographics@3.0.0. Перед стабильным релизом обкатал новую мажорную версию в реальных проектах. Главное изменение оказалось не в новой текстовой роли и не в ещё одном CSS-свойстве. Убрал из библиотеки старое правило:
html {
font-size: 10px;
}
Этот приём много лет жил в моих проектах ради простой арифметики: 1.6rem легко читать как 16px. Для собственного проекта это удобно. Для переиспользуемой библиотеки уже нет — typographics менял корневой размер документа и начинал спорить с CSS проектов.
В 3.0.0 решил отказаться от привязки расчётов к фиксированным 10px. Библиотека должна учитывать корневой размер шрифта, который задаёт приложение.
Без привязки к корню 10px
До этого плавная шкала была завязана на 10px сразу в нескольких местах. Даже границы 600px и 1440px приходилось переводить в rem через деление на 10:
--t-font-scale-min-width-rem:
calc((var(--t-font-scale-min-width, 600) / 10) * 1rem);
html {
font-size: 10px;
}
Удобная локальная договорённость незаметно стала частью контракта библиотеки. Если проект использовал обычный корневой размер браузера или задавал свой, typographics всё равно приходил со своими 10px.
В 3.0.0 вместо фиксированных 10px в слое reset используется font-size: 100%:
@layer reset {
html {
font-size: 100%;
text-size-adjust: 100%;
box-sizing: border-box;
}
}
rem по-прежнему отсчитывается от размера шрифта корневого элемента. Для расчёта плавной шкалы больше не нужно считать, что 1rem равен 10px.
Поэтому пришлось пересобрать и саму модель размеров.
Одного clamp() уже мало
Раньше у пакета была одна база --t-font-size-clamp. Крупные роли росли относительно неё через em, а часть ролей основного текста оставалась фиксированной в rem. После отказа от корня 10px решил разделить шкалы: у основного текста и у заголовков теперь свои плавные базы.
Ниже — параметры двух шкал, без полной формулы clamp():
:root {
--t-body-font-size-min-scale: 0.875;
--t-body-font-size-max-scale: 1.125;
--t-body-font-size-min:
calc(var(--t-body-font-size-min-scale) * 1rem);
--t-body-font-size-max:
calc(var(--t-body-font-size-max-scale) * 1rem);
--t-body-font-size-clamp: clamp(/* ... */);
--t-heading-font-size-min-scale: 1;
--t-heading-font-size-max-scale: 1.25;
--t-heading-font-size-min:
calc(var(--t-heading-font-size-min-scale) * 1rem);
--t-heading-font-size-max:
calc(var(--t-heading-font-size-max-scale) * 1rem);
--t-heading-font-size-clamp: clamp(/* ... */);
}
Причина разделения практическая. Заголовкам обычно нужен больший диапазон изменения размера, чем абзацам. При общей базе изменение её границ влияет и на заголовки, и на основной текст.
При корневом размере шрифта 16px и линейном изменении между 600px и 1440px базы выглядят так:
| ширина | база текста | база заголовков |
|---|---|---|
600px |
14px |
16px |
1020px |
16px |
18px |
1440px |
18px |
20px |
Сами роли теперь не хранят готовый размер. Они задают коэффициент:
--t-display-large: 5.7;
--t-headline-large: 3.2;
--t-title-medium: 2;
Миксин заголовка умножает коэффициент на базу заголовков:
font-size: calc(
var(--t-heading-font-size-clamp) * #{$k}
);
Для абзацев и списков используется база основного текста:
font-size: calc(
var(--t-body-font-size-clamp) * #{$k}
);
Например, h2 соответствует headline-large с коэффициентом 3.2. При ширине 600px это 16px × 3.2 = 51.2px, при 1440px — 20px × 3.2 = 64px. База основного текста за тот же диапазон меняется с 14px до 18px. В пикселях размер заголовка меняется заметно сильнее, хотя обе шкалы используют одни и те же границы.
Теперь это две независимые настройки. Если в конкретном проекте заголовки должны расти сильнее или слабее, для этого больше не нужно менять поведение основного текста.
Семантическая разметка без тяжёлых селекторов
Параллельно с новой шкалой пересобрал CSS-правила typographics. Пакет всё чаще используется как базовые стили документа, поэтому странно требовать класс для каждого обычного h2 или p. В 3.0.0 обычные HTML-элементы тоже получают стили текстовых ролей:
:where(h1, .display-large) {
@include typography-heading(var(--t-display-large));
}
:where(h2, .headline-large) {
@include typography-heading(var(--t-headline-large));
}
:where(p, .body-medium) {
@include typography-paragraph(1em, 1.45, 400);
}
Можно написать обычную статью с h1, h2 и p и сразу получить базовую типографику. Классы ролей остаются доступны для случаев, когда семантика и визуальная роль не совпадают.
Вторая половина решения — :where(). У него нулевая специфичность, поэтому базовое правило для HTML-элемента легко переопределить в CSS проекта.
По той же причине весь CSS теперь разложен по слоям каскада:
@layer reset, tokens, typography, components, utilities;
Заполнены слои tokens, reset и typography. Обычные декларации CSS проекта вне слоёв имеют приоритет над обычными декларациями внутри слоёв. Получается нужное для библиотечного CSS поведение: typographics задаёт основу, но не пытается выиграть каскад у приложения любой ценой.
@layer и :where() решают разные задачи. Слои задают порядок групп правил в каскаде, а :where() не раздувает специфичность конкретных правил.
Обкатка перед релизом
Перед 3.0.0 обкатал 3.0.0-dev.0 и 3.0.0-dev.1 на реальных проектах.
В финальном варианте оставил старые границы 600px и 1440px, но сама система вокруг них теперь другая. Проект может переопределить корневой размер шрифта. Основной текст и заголовки масштабируются независимо. Семантические элементы получают базовые значения, которые легко переопределить в CSS проекта.
Библиотеке больше не нужен фиксированный корневой размер шрифта, чтобы удобно считать типографику.