![](https://habrastorage.org/files/b15/4e3/f69/b154e3f690cf4709957005f22f02b096.png)
CSS-шлюзом (CSS-lock) называется методика из адаптивного веб-дизайна, позволяющая не перепрыгивать от одного значения к другому, а переходить плавно, в зависимости от текущего размера области просмотра (viewport). Идею и одну из реализаций предложил Тим Браун в статье Flexible typography with CSS locks. Когда я пытался разобраться с его реализацией и создать свои варианты, мне с трудом удавалось понять, что именно происходит. Я выполнил много вычислений и подумал, что полезно будет объяснить другим всю эту математику.
В статье я опишу саму методику, её ограничения и лежащую в её основе математику. Не волнуйтесь: там в основном одни сложения и вычитания. К тому же я постарался всё разбить на этапы и украсил их графиками.
Что такое CSS-шлюз?
Зависимость от размера области просмотра
В моих последних проектах использовался баннер на всю ширину с заголовком и одни лишь «десктопные» шаблоны с крупными шрифтами. Я решил, что мне нужны маленькие шрифты для небольших экранов и что-то для экранов промежуточных размеров. Так почему бы не поставить размер шрифтов в зависимость от ширины области просмотра?
Раньше это делали примерно так:
h1 { font-size: 4vw; /* Бум! Готово. */ }
У этого подхода есть две проблемы:
- На очень маленьких экранах текст становится крохотным (12,8 пикселя в высоту при ширине экрана 320 пикселей), на больших — огромным (64 при 1600);
- Не учитываются пользовательские настройки размера шрифта.
CSS-шлюзы позволяют избавиться от первой проблемы. Замечательные CSS-шлюзы также постараются учитывать и пользовательские настройки.
Идея CSS-шлюза
CSS-шлюз — это особый вид вычисления CSS-значения, при котором:
- есть минимальное и максимальное значение,
- есть две контрольные точки (breakpoint) (обычно зависят от ширины области просмотра),
- между этими точками значение меняется линейно от минимума до максимума.
![](https://habrastorage.org/files/ca5/71e/a9b/ca571ea9b08d4f539666360eb2bc4d9b.png)
«При ширине меньше 320 пикселей будем использовать шрифты 20px, свыше 960 пикселей — 40px, а между 320 и 960 — от 20px до 40px».
На стороне CSS это может выглядеть так:
h1 { font-size: 1.25rem; }
@media (min-width: 320px) {
h1 { font-size: /* волшебное значение от 1.25 rem до 2.5 rem */; }
}
@media (min-width: 960px) {
h1 { font-size: 2.5rem; }
}
Первая задача — реализовать волшебное значение. Немного подпорчу вам удовольствие и сразу скажу, что это выглядит так:
h1 {
font-size: calc(1.25rem + viewport_relative_value);
}
Здесь
viewport_relative_value
может быть одиночным значением (например, 3vw
) или представлять собой более сложное вычисление (на основе единицы измерения области просмотра vw
или какой-то другой единицы).Ограничения
Поскольку CSS-шлюзы завязаны на единицы измерения области просмотра, шлюзы имеют ряд важных ограничений. Они могут принимать только числовые значения, использовать
calc()
и принимать значения в пикселях.Почему так? Потому что единицы измерения области просмотра (
vw
, vh
, vmin
и vmax
) всегда определяются в пикселях. Например, если ширина области просмотра — 768 пикселей, то 1vw
определяется в 7,68 пикселя.(В статье Тима есть ошибка: он пишет, что вычисления вроде
100vw - 30em
дают значение em
. Это не так. Браузер считает 100vw
в пикселях и вычитает из него значение 30em
для этого элемента и свойства.) Некоторые примеры того, что не работает:
- CSS-шлюз для свойства
opacity
, потому чтоopacity: calc(.5+1px)
является ошибкой; - CSS-шлюз для большинства функций
transform
(например,rotate
: шлюз не может выполнять вращение на основании значения в пикселях).
Итак, у нас есть пиксельное ограничение. Может быть, найдутся храбрецы и вычислят все свойства и методики, которые могут использоваться в CSS-шлюзах.
Для начала возьмём свойства
font-size
и line-height
и посмотрим, как можно создавать для них CSS-шлюзы с контрольными точками на основе пикселей или em.CSS-шлюзы с пиксельными контрольными точками
Демки
- CSS calc lock for font-size (rem+px, px MQ)
- CSS calc lock for line-height (%+px, px MQ)
- Combined font-size and line-height lock (px-based)
Далее мы рассмотрим, как получить CSS-код для каждого из этих примеров.
Размер шрифта как линейная функция
Нам нужно, чтобы font-size пропорционально увеличивался с 20px при ширине области 320px до 40px при ширине 960px. Отразим это на графике:
![](https://habrastorage.org/files/d9a/10e/267/d9a10e267b7e4b1888822742311eb49d.png)
Красная линия — это график линейной функции. Можно записать её как
y = mx + b
:y
— размер шрифта (вертикальная ось),x
— ширина области просмотра в пикселях (горизонтальная ось),m
— наклон (slope) функции (сколько пикселей мы добавляем к размеру шрифта при увеличении ширины области просмотра на 1 пиксель),b
— размер шрифта до того, как мы начинаем увеличивать размер области просмотра.
Нам нужно вычислить
m
и b
. В уравнении они являются константами.Сначала разберёмся с
m
. Для этого нужны только координаты (x,y)
. Это похоже на вычисление скорости (дистанция, пройденная за единицу времени), но в данном случае мы вычисляем размер шрифта в зависимости от ширины области просмотра:m = font_size_increase / viewport_increase
m = (y2 - y1) / (x2 - x1)
m = (40 - 20) / (960 - 320)
m = 20 / 640
m = 0.03125
Другая форма:
Общее увеличение font-size — 20 пикселей (
40 - 20
).Общее уменьшение области просмотра — 640 пикселей (
960 - 320
).Если ширина области вырастет на 1 пиксель, насколько увеличится размер font-size?
20 / 640 = 0.03125 px.
Теперь вычислим
b
.y = mx + b
b = y - mx
b = y - 0.03125x
Поскольку наша функция проверяется с помощью обеих этих точек, мы можем использовать координаты
(x,y)
любой из них. Возьмём первую:b = y1 - 0.03125 × x1
b = 20 - 0.03125 × 320
b = 10
Кстати, вычислить эти 10 пикселей можно было, просто посмотрев на график. Но ведь он не всегда у нас есть :-)
Теперь наша функция выглядит так:
y = 0.03125x + 10
Преобразование в CSS
y
— размер font-size, и, если мы хотим выполнить базовые операции в CSS, нам нужен calc()
.font-size: calc( 0.03125x + 10px );
Конечно, это псевдоCSS, потому
x
не является валидным синтаксисом. Но в нашей линейной функции x
представляет ширину области просмотра, которую в CSS можно выразить как 100vw
.font-size: calc( 0.03125 * 100vw + 10px );
Вот теперь получился работающий CSS. Если нужно выразить более кратко, выполним умножение. Поскольку
0.03125 × 100 = 3.125
, то:font-size: calc( 3.125vw + 10px );
Теперь ограничим ширину области просмотра 320 и 960 пикселями. Добавим несколько media-запросов:
h1 { font-size: 20px; }
@media (min-width: 320px) {
h1 { font-size: calc( 3.125vw + 10px ); }
}
@media (min-width: 960px) {
h1 { font-size: 40px; }
}
Теперь наш график выглядит как нужно:
![](https://habrastorage.org/files/ca5/71e/a9b/ca571ea9b08d4f539666360eb2bc4d9b.png)
Красиво, но мне не очень нравятся значения в пикселях при объявлении font-size. Можно ли сделать лучше?
Применение пользовательских настроек
Практически каждый браузер позволяет пользователям задавать размер текста по умолчанию. Чаще всего оно изначально равно 16 пикселям, но иногда его изменяют (обычно увеличивают).
Я хочу вставить пользовательские настройки в нашу формулу и для этого обращу внимание на значения
rem
. В отношении em
и процентных значений применяется тот же принцип.Сначала проверим, что базовому (root) font-size не присвоено абсолютное значение. Например, если вы используете CSS из Bootstrap 3, там встречается немало такого:
html {
font-size: 10px;
}
Никогда так не делайте! (К счастью, в Bootstrap 4 это исправили.) Если вам действительно нужно изменить значение базового em (
1rem
), используйте:/*
* Меняет значение rem с соблюдением пропорциональности.
* При размере по умолчанию font-size 16 пикселей:
* • 62.5% -> 1rem = 10px, .1rem = 1px
* • 125% -> 1rem = 20px, .05rem = 1px
*/
html {
font-size: 62.5%;
}
Тем не менее оставим в покое базовый font-size, пусть он будет по умолчанию равен 16 пикселям. Давайте посмотрим, что произойдёт, если в нашем font-size-шлюзе заменить пиксельные значения
rem
-значениями:/*
* С пользовательскими настройками по умолчанию:
* • 0.625rem = 10px
* • 1.25rem = 20px
* • 2.5rem = 40px
*/
h1 { font-size: 1.25rem; }
@media (min-width: 320px) {
h1 { font-size: calc( 3.125vw + .625rem ); }
}
@media (min-width: 960px) {
h1 { font-size: 2.5rem; }
}
Если запустить код с браузерными настройками по умолчанию, то он будет вести себя, как предыдущий код, использовавший пиксели. Замечательно!
Но поскольку мы сделали это ради поддержки вносимых пользователями изменений, нужно проверить, как всё работает. Допустим, пользователь задал размер шрифта 24 пикселя вместо 16 (на 50 % больше). Как поведёт себя код?
![](https://habrastorage.org/files/73f/fd2/55b/73ffd255be3f4526a949e312f82866c2.png)
Синяя линия: font-size по умолчанию равен 16 пикселям.
Красная линия: font-size по умолчанию равен 24 пикселям.
При увеличении области просмотр до 320 пикселей шрифт становится меньше (с 30 пикселей уменьшается до 25), а при достижении второй контрольной точки увеличивается скачкообразно (с 45 до 60 пикселей). Ой.
Исправить это поможет то же настраиваемое пользователем базовое значение (baseline) для всех трёх размеров. Например, выберем
1.25rem
:h1 { font-size: 1.25rem; }
@media (min-width: 320px) {
h1 { font-size: calc( 1.25rem + 3.125vw - 10px ); }
}
@media (min-width: 960px) {
h1 { font-size: calc( 1.25rem + 20px ); }
}
Обратите внимание на
3.125vw - 10px
. Это наша старая линейная функция (в виде mx + b
), но с другим значением b
. Назовём его b′
. В данном случае мы знаем, что базовое значение равно 20 пикселям, поэтому можем получить значение b′
простым вычитанием:b′ = b - baseline_value
b′ = 10 - 20
b′ = 10
Другой способ — выбирать базовое значение с самого начала, а потом искать линейную функцию, описывающую увеличение font-size (назовём его
y′
, чтобы не путать со значением самого font-size y
).x1 = 320
x2 = 960
y′1 = 0
y′2 = 20
m = (y′2 - y′1) / (x2 - x1)
m = (20 - 0) / (960 - 320)
m = 20 / 640
m = 0.03125
b′ = y′ - mx
b′ = y′1 - 0.03125 × x1
b′ = 0 - 0.03125 × 320
b′ = -10
Получили функцию
y′ = 0.03125x - 10
, которая выглядит так:![](https://habrastorage.org/files/069/331/63d/06933163d15742dca038534d7b32096a.png)
С базовым значением в
rem
и дополнительными значениями в vw
и/или px
мы наконец-то можем создать полноценный работающий шлюз для font-size. Когда пользователь меняет размер шрифта по умолчанию, система подстраивается под него и не ломается. ![](https://habrastorage.org/files/17a/cae/604/17acae604fa342079ffcb35098381e2f.png)
Пурпурная линия: степень увеличения font-size.
Синяя линия: font-size по умолчанию равен 16 пикселям.
Красная линия: font-size по умолчанию равен 24 пикселям.
Конечно, это не совсем то, что просил пользователь: он хотел увеличить шрифт на 50%, а мы увеличили его на 50% в маленьких областях просмотра и на 25% — в больших. Но это хороший компромисс.
Создание шлюза для высоты строки
В данном случае у нас будет такой сценарий: «Я хочу параграфы с высотой строки в 140% при области просмотра шириной 320 пикселей и 180% — при 960».
Поскольку мы будем работать с базовым значением плюс динамически изменяемым значением, выраженным в пикселях, нам нужно знать, сколько пикселей составляют коэффициенты 1,4 и 1,8. То есть нужно вычислить
font-size
для наших параграфов. Допустим, базовый размер шрифта равен 16 пикселям. Получаем:16 * 1.4 = 22.4
пикселя при нижнем размере области просмотра (320 px
)16 * 1.8 = 28.8
пикселя при верхнем размере области просмотра (960 px
)
В качестве базового значения возьмём
140% = 22.4px
. Получается, что общее увеличение шрифта составляет 6,4 пикселя. Воспользуемся нашей линейной формулой:x1 = 320
x2 = 960
y′1 = 0
y′2 = 6.4
m = (y′2 - y′1) / (x2 - x1)
m = (6.4 - 0) / (960 - 320)
m = 6.4 / 640
m = 0.01
b′ = y′ - mx
b′ = y′1 - 0.01 × x1
b′ = 0 - 0.01 × 320
b′ = 3.2
y′ = 0.01x - 3.2
Преобразуем в CSS:
line-height: calc( 140% + 1vw - 3.2px );
Примечание: базовое значение нужно выражать как 140% или
1.4em
; безразмерное 1.4
не будет работать внутри calc()
.Затем добавляем media-запросы и проверяем, чтобы все объявления
line-height
использовали одно базовое значение (140%
).p { line-height: 140%; }
@media (min-width: 320px) {
p { line-height: calc( 140% + 1vw - 3.2px ); }
}
@media (min-width: 960px) {
p { line-height: calc( 140% + 6.4px ); }
}
Напоминаю: для большой области просмотра нельзя использовать просто
180%
, нам нужна выраженная в пикселях часть, которая добавляется к базовому значению. Если взять 180%
, то при базовом размере шрифта 16 пикселей всё будет нормально, пока пользователь его не поменяет.Построим график и проверим работу кода при разных базовых значениях font-size.
![](https://habrastorage.org/files/a4c/357/6c6/a4c3576c6f834b1c9d727aa8cb219744.png)
Синяя линия: font-size по умолчанию равен 16 пикселям.
Красная линия: font-size по умолчанию равен 24 пикселям.
Теперь, когда наша формула
line-height
зависит от собственного размера font-size элемента, изменение размера шрифта приведёт к изменению формулы. Например, в этом демо показан параграф с увеличенным текстом, определённым как:.big {
font-size: 166%;
}
Это меняет наши контрольные точки:
16 * 1.66 * 1.4 = 37.184
пикселя при нижнем размере области просмотра (320px
)16 * 1.66 * 1.8 = 47.808
пикселя при верхнем размере области просмотра (960px
)
Проведём вычисления и получим обновлённую формулу:
y′ = 0.0166x - 5.312
. Затем объединим в CSS этот и предыдущий стили:p { line-height: 140%; }
.big { font-size: 166%; }
@media (min-width: 320px) {
p { line-height: calc( 140% + 1vw - 3.2px ); }
.big { line-height: calc( 140% + 1.66vw - 5.312px ); }
}
@media (min-width: 960px) {
p { line-height: calc( 140% + 6.4px ); }
.big { line-height: calc( 140% + 10.624px ); }
}
Также можно возложить вычисления на CSS. Раз мы используем одни и те же контрольные точки и относительные размеры line-heights, как для стандартного параграфа, то нам нужно лишь добавить коэффициент 1,66:
p { line-height: 140%; }
.big { font-size: 166%; }
@media (min-width: 320px) {
p { line-height: calc( 140% + 1vw - 3.2px ); }
.big { line-height: calc( 140% + (1vw - 3.2px) * 1.66 ); }
}
@media (min-width: 960px) {
p { line-height: calc( 140% + 6.4px ); }
.big { line-height: calc( 140% + 6.4px * 1.66 ); }
}
Объединение шлюзов font-size и line-height
Попробуем теперь всё собрать воедино. Сценарий: есть адаптирующийся столбец текста (fluid column) с H1 и несколькими параграфами. Нам нужно изменить font-size и line-height, используя следующие значения:
Элемент и свойство | Значение при 320px | Значение при 960px |
H1 font-size | 24 пикселя | 40 пикселей |
H1 line-height | 133,33% | 120% |
P font-size | 15 пикселей | 18 пикселей |
P line-height | 150% | 166,67% |
Вы увидите, что с высотой строки мы делаем две вещи. Есть общее правило: когда текст увеличивается, высоту строки нужно уменьшать, а когда столбец становится шире — увеличивать. Но в нашем сценарии одновременно происходят обе ситуации, противоречащие друг другу! Поэтому нужно выбрать приоритеты:
- Для H1 увеличение font-size будет критичнее, чем увеличение ширины столбца.
- Для параграфов увеличение ширины столбца будет критичнее, чем небольшое увеличение font-size.
Теперь выберем две контрольные точки — область просмотра шириной 320 и 960 пикселей. Начнём с написания шлюза для font-size:
h1 { font-size: 1.5rem; }
/* .9375rem = 15px с настройками по умолчанию */
p { font-size: .9375rem; }
@media (min-width: 320px) {
h1 { font-size: calc( 1.5rem + 2.5vw - 8px ); }
/* .46875vw - 1.5px равно от 0 до 3px */
p { font-size: calc( .9375rem + .46875vw - 1.5px ); }
}
@media (min-width: 960px) {
h1 { font-size: calc(1.5rem + 16px); }
p { font-size: calc( .9375rem + 3px ); }
}
Здесь ничего нового, только значения поменялись.
Теперь рассчитываем шлюзы для
line-height
. Это будет куда сложнее, чем в прошлый раз.Начнём с элемента H1. Для
line-height
воспользуемся относительным базовым значением — 120%. Поскольку размер шрифта элемента у нас изменяем, то эти 120% позволяют описать динамическое и линейное значение, определяемое двумя точками:24 × 1.2 = 28.8px
в нижней контрольной точке,40 × 1.2 = 48px
в верхней контрольной точке.
В нижней контрольной точке нам нужно иметь значение
line-height
, равное 133,33%, это около 32 пикселей.Найдём линейную функцию, описывающую «то, что добавляется к базовому значению 120%». Если убрать эти 120%, получим два модифицированных значения:
24 × (1.3333 - 1.2) = 3.2px
в нижней контрольной точке,40 × (1.2 - 1.2) = 0px
в верхней контрольной точке.
Должен получиться отрицательный наклон.
m = (y′2 - y′1) / (x2 - x1)
m = (0 - 3.2) / (960 - 320)
m = -3.2 / 640
m = -0.005
b′ = y′ - mx
b′ = y′1 - (-0.005 × x1)
b′ = 3.2 + 0.005 × 320
b′ = 4.8
y′ = -0.005x + 4.8
Преобразуем в CSS:
h1 {
line-height: calc( 120% - .5vw + 4.8px );
}
Посмотрим на график:
![](https://habrastorage.org/files/ab3/6d4/ab7/ab36d4ab713a40b48844be4e28b75704.png)
Синяя линия: уменьшение line-height.
Красная линия: базовое значение line-height (120% font-size заголовка).
Пурпурная линия: финальная line-height.
На графике видно, что результирующая высота строки (пурпурная линия) равна базовому значению 120% плюс уменьшение высоты строки (синяя линия). Можете сами проверить вычисления на GraphSketch.com.
Для параграфов мы воспользуемся базовым значением
150%
. Увеличение line-height:(1.75 - 1.5) × 18 = 4.5px.
![](https://habrastorage.org/files/ccb/9d2/50f/ccb9d250f21a4ae4894315d0e2d3f42c.png)
Мой калькулятор говорит, что формула будет такая:
y′ = 0.00703125x - 2.25
Чтобы увидеть полный CSS-код, взгляните на исходный код демки, в которой объединены font-size и line-height. Изменяя размер браузерного окна, вы убедитесь, что эффект есть, хоть и слабый.
Также рекомендую потестировать эту демку, изменив размер шрифта по умолчанию. Обратите внимание, что здесь соотношения line-height будут немного другими, но вполне допустимыми. Нет ничего плохого в том, что line-height становится меньше базового значения.
Автоматизация вычислений
Готовя этот раздел, я выполнял все вычисления вручную либо с помощью калькулятора Soulver. Но это довольно трудоёмко, и высока вероятность ошибок. Чтобы исключить человеческий фактор, хорошо бы внедрить автоматизацию.
Первый способ — перенести все вычисления в CSS. Это вариант формулы, использованной в примерах с font-size, когда подробно разбирались все значения:
@media (min-width: 320px) and (max-width: 959px) {
h1 {
font-size: calc(
/* y1 */
1.5rem
/* + m × x */
+ ((40 - 24) / (960 - 320)) * 100vw
/* - m × x1 */
- ((40 - 24) / (960 - 320)) * 320px
);
}
}
Но получается слишком много букв, можно написать гораздо лаконичней:
@media (min-width: 320px) and (max-width: 959px) {
h1 {
font-size: calc( 1.5rem + 16 * (100vw - 320px) / (960 - 320) );
}
}
Так совпало, что эту формулу использовал Тим Браун в статье Flexible typography with CSS locks, правда, с пикселями вместо
em
в значении переменной. Это работает и для объединённого варианта с font-size и line-height, но может быть не столь очевидно, особенно при отрицательном наклоне.@media (min-width: 320px) and (max-width: 959px) {
h1 {
font-size: calc( 1.5rem + 16 * (100vw - 320px) / (960 - 320) );
/* При отрицательном наклоне нужно инвертировать контрольные точки */
line-height: calc( 120% + 3.2 * (100vw - 960px) / (320 - 960) );
}
}
Второй способ — автоматизировать вычисления с помощью плагина Sass или PostCSS mixin.
CSS-шлюзы с em-контрольными точками
Новые демки
Я взял три первые демки и вместо пиксельных значений контрольных точек и инкрементирований вставил значения на основе
rem
.- CSS calc lock for font-size (rem+rem, em MQ)
- CSS calc lock for line-height (%+rem, px MQ)
- Combined font-size and line-height lock (em/rem-based)
В следующем разделе мы рассмотрим работу специфического синтаксиса в этих демках.
Синтаксис m × 100vw для media-запросов на основе em — не лучшая идея
Выше мы задействовали синтаксис
m × 100vw
(например, здесь calc(base + 2.5vw)
). Его нельзя использовать с media-запросами на основе em
.Всё дело в контексте media-запросов. Единицы
em
и rem
ссылаются на одно и то же: базовый размер шрифта в User Agent. А он, как мы уже несколько раз видели, обычно равен 16 пикселям, но значение может быть и другое. Почему?- По воле браузера или ОС (в основном в специфических случаях вроде ТВ-браузеров и читалок).
- По воле пользователя.
Так что если у нас контрольные точки
20em
и 60em
, то они будут соответствовать реальной CSS-ширине:- 320 и 960 пикселей при базовом размере шрифта 16 пикселей,
- 480 и 1440 — при 24 пикселях и т. д.
(Обратите внимание, что это CSS-пиксели, а не аппаратные пиксели. В статье мы не рассматриваем аппаратные пиксели, поскольку они не влияют на наши вычисления.)
Выше приводились примеры кода наподобие такого:
font-size: calc( 3.125vw + .625rem );
Если в этом синтаксисе заменить все контрольные точки с использованием
em
, приняв, что в media-запросе 1 em равен 16 пикселям, то получится:h1 { font-size: 1.25rem; }
/* Не делайте так :((( */
@media (min-width: 20em) {
h1 { font-size: calc( 1.25rem + 3.125vw - 10px ); }
}
/* Или так. */
@media (min-width: 60em) {
h1 { font-size: calc( 1.25rem + 20px ); }
}
Это сработает, если ОС, браузер и пользователь никогда не меняют размер шрифта по умолчанию. А иначе будет плохо:
![](https://habrastorage.org/files/74a/96b/9ad/74a96b9ade504fffaf9bff000b0ad9f0.png)
Синяя линия: font-size по умолчанию равен 16 пикселям.
Красная линия: font-size по умолчанию равен 24 пикселям.
Что здесь происходит? Когда мы меняем базовый font-size, контрольные точки на основе
em
смещаются на более высокие пиксельные значения. Единственно верным значением для конкретных точек будет 3.125vw - 10px
!- При 320 пикселях
3.125vw - 10px
равно 0 пикселям, как и должно быть. - При 480 пикселях
3.125vw - 10px
равно 5 пикселям.
На высоких контрольных точках ещё хуже:
- При 960 пикселях
3.125vw - 10px
равно 20 пикселям, как и должно быть. - При 1440 пикселях
3.125vw - 10px
равно 35 пикселям (на 15 больше).
Если вы хотите использовать контрольные на основе
em
, то нужно делать иначе.Снова выполняем расчёты
Эта методика продемонстрирована в статье Тима Брауна. Она подразумевает, что большинство вычислений делается в CSS с использованием двух переменных частей:
100vw
— ширина области просмотра;- нижняя контрольная точка, выраженная в
rem
.
Воспользуемся формулой:
y = m × (x - x1) / (x2 - x1)
Почему именно ней? Давайте разберём по шагам. Выше мы увидели, что font-size и line-height можно описать линейной функцией:
y = mx + b
В CSS можно работать с
x
(это 100vw
). Но нельзя задать m
и b
точные значения в пикселях или vw
, потому что это константы, выраженные в пикселях, и их можно спутать с нашими контрольными точками, выраженными в em
, если пользователь изменит размер шрифта по умолчанию.Попробуем заменить
m
и b
другими известными значениями, а именно (x1,y1)
и (x2,y2)
.Находим
b
с помощью первой пары координат: b = y - mx
b = y1 - m × x1
Собираем всё вместе:
y = mx + b
y = mx + y1 - m × x1
Мы исключили
b
из формулы!Также выше мы видели, что на самом деле нам нужно было не полное значение
font-size
или line-height
, а только динамическая часть, которую мы добавляем к базовому значению. Мы назвали её y′
и можем выразить так:y = y1 + y′
y′ = y - y1
Заменим
y
с помощью выведенного равенства:y′ = mx + y1 - m × x1 - y1
y′ = mx + y1 - m × x1 - y1
Мы можем избавиться от кусков
+ y1
и - y1
!y′ = m × x - m × x1
y′ = m × (x - x1)
Теперь можем заменить
m
уже известными значениями:m = (y2 - y1) / (x2 - x1)
Тогда:
y′ = (y2 - y1) / (x2 - x1) × (x - x1)
Также это можно записать так:
y′ = max_value_increase × (x - x1) / (x2 - x1)
Преобразование в CSS
Это значение мы можем использовать в CSS. Вернёмся опять к нашему примеру «от 20 до 40 пикселей»:
@media (min-width: 20em) and (max-width: 60em) {
h1 {
/* ВНИМАНИЕ: это пока не работает! */
font-size: calc(
1.25rem /* базовое значение */
+ 20px /* разница между максимальным и базовым значениями */
* (100vw - 20rem) /* x - x1 */
/ (60rem - 20rem) /* x2 - x1 */
);
}
}
Код пока не работает. Кажется, что он мог бы работать, но
calc()
в CSS имеет ряд ограничений, относящихся к умножению и делению.Начнём с фрагмента
100vw - 20rem
. Эта часть работает как есть и возвращает значение в пикселях.Например, если font-size по умолчанию — 16 пикселей, а ширина области просмотра — 600 пикселей, то результат равен 280 пикселям (
600 - 20 × 15
). Если font-size по умолчанию — 24 пикселя, а ширина области просмотра — 600 пикселей, то результат равен 120 пикселям (600 - 20 × 24
). ![](https://habrastorage.org/files/fd0/517/82c/fd051782ced047da88e4216d47ffad9e.png)
Обратите внимание, что для выражения наших контрольных точек мы используем единицу
rem
. Почему не em
? Потому что в CSS-значении em
ссылается не на базовый font-size, а на собственный font-size элемента (в общем) либо на его родительский font-size (когда используется свойство font-size
).В идеале нам нужна CSS — единица измерения, ссылающаяся на браузерный размер шрифта по умолчанию. Но такой единицы не существует. Самое близкое — это
rem
, которая ссылается на базовый font-size, только если он абсолютно не менялся.То есть в нашем CSS ни в коем случае не должно быть подобного кода:
/* Плохо */
html { font-size: 10px; }
/* Достаточно плохо */
:root { font-size: 16px; }
/* Удовлетворительно, но придётся прописать все
ключевые точки, например 20rem/1.25,
40em/1.25 и т. д. */
:root { font-size: 125%; }
Безразмерные знаменатели и множители calc
Хотелось бы привести
60rem - 20rem
к ширине в пикселях. Это означало бы, что дробь (x - x1) / (x2 - x1)
давала бы значение в диапазоне от 0 до 1. Назовём его n
.При размере шрифта по умолчанию в 16 пикселей и ширине области просмотра в 600 пикселей мы получим:
n = (x - x1) / (x2 - x1)
n = (600 - 320) / (960 - 320)
n = 280 / 640
n = 0.475
К сожалению, не совсем то.
Проблема в том, что при делении в
calc()
мы не можем в качестве знаменателя использовать пиксели или какую-либо CSS-единицу. Величина должна быть безразмерной. Так что нам делать?А что если просто убрать единицы измерения в знаменателе? Каков будет результат вычисления
calc((100vw - 20rem)/(60 - 20))
?Размер шрифта по умолчанию — 16 пикселей | ||
Область просмотра | Деление в CSS | Результат |
20em (320px) | (320px – 16px × 20) / (60 – 20) | = 0px |
40em (640px) | (640px – 16px × 20) / (60 – 20) | = 8px |
60em (960px) | (960px – 16px × 20) / (60 – 20) | = 16px |
Размер шрифта по умолчанию — 24 пикселя | ||
Область просмотра | Деление в CSS | Результат |
20em (480px) | (480px – 24px × 20) / (60 – 20) | = 0px |
40em (960px) | (960px – 24px × 20) / (60 – 20) | = 12px |
60em (1440px) | (1440px – 24px × 20) / (60 – 20) | = 24px |
Как видите, в диапазоне между контрольными точками (от
20em
до 60em
) мы получаем линейное изменение от 0rem
до 1rem
. Годится!При первой попытке заставить работать наш CSS мы использовали множитель
20px
. Надо его вычеркнуть.Код первой попытки:
font-size: calc( 1.25rem + 20px * n );
Здесь
n
принимала значение от 0 до 1. Но из-за ограничений синтаксиса деления в calc()
мы не могли получать нужный нам результат от 0 до 1.Тогда мы получили пиксельный эквивалент для диапазона
0rem — 1rem
; назовём это значение r
.Другое ограничение
calc()
относится к умножению. Если записать calc(a * b)
, то a
или b
должно быть безразмерным числом. Поскольку у
r
есть размерность (это пиксели), то безразмерным должен быть второй множитель.Мы хотим увеличить на 20 пикселей в верхней контрольной точке. 20 пикселей — это
1.25rem
, так что множитель будет 1.25
:font-size: calc( 1.25rem + 1.25 * r );
Должно сработать. Но имейте в виду, что значение
r
будет меняться в зависимости от размера шрифта по умолчанию:- 16 пикселей:
1.25 * r
равно от 0 до 20 пикселей. - 24 пикселя:
1.25 * r
равно от 0 до 30 пикселей.
Давайте теперь напишем весь CSS-шлюз целиком, с media-запросами, верхним и нижним значениями:
h1 {
font-size: 1.25rem;
}
@media (min-width: 20em) {
/* Результат (100vw - 20rem) / (60 - 20) в диапазоне 0-1rem,
в зависимости от ширины области просмотра (от 20em до 60em). */
h1 {
font-size: calc( 1.25rem + 1.25 * (100vw - 20rem) / (60 - 20) );
}
}
@media (min-width: 60em) {
/* Правая часть дополнения ДОЛЖНА быть rem.
В нашем примере мы МОЖЕМ заменить всё объявление
на font-size: 2.5rem, но если наше базовое значение
выражалось не в rem, то придётся использовать calc. */
h1 {
font-size: calc( 1.25rem + 1.25 * 1rem );
}
}
В этом случае, в отличие от шлюза font-size, использующего пиксели, когда пользователь увеличивает размер шрифта по умолчанию на 50%, всё остальное тоже увеличивается на 50%: базовое значение, переменная и контрольные точки. Мы получаем диапазон 30—60 пикселей вместо необходимого 20—40.
![](https://habrastorage.org/files/ba4/530/acd/ba4530acd1074909acabca6b3fc9b7bf.png)
Синяя линия: font-size по умолчанию равен 16 пикселям.
Красная линия: font-size по умолчанию равен 24 пикселям.
Можете проверить это самостоятельно в первой демке, использующей em.
Шлюзы line-height c em/rem
В нашей второй демке мы изменим
line-height
параграфа со 140% до 180%. Будем использовать 140%
в качестве базового значения, а в роли переменной части выступит та же формула, что и в примере с font-size
.p {
line-height: 140%;
}
@media (min-width: 20em) {
p {
line-height: calc( 140% + .4 * (100vw - 20rem) / (60 - 20) );
}
}
@media (min-width: 60em) {
p {
line-height: calc( 140% + .4 * 1rem );
}
}
Для переменной части
line-height
нам нужно rem-значение, потому что (100vw - 20rem) / (60 - 20)
даёт результат в пикселях в диапазоне от 0rem
до 1rem
.Поскольку
font-size
нашего параграфа остаётся равен 1rem
, увеличение высоты строки ещё на 40%
равно .4rem
. Это значение мы и будем использовать в двух calc()
-выражениях.Теперь возьмём из третьей демки пример с
line-height
. Нам нужно уменьшить line-height
H1 со 133,33% до 120%. И мы знаем, что при этом изменится font-size
.Для того же примера мы уже определяли, что уменьшение высоты строки можно выразить с помощью двух контрольных точек:
24 × (1.3333 - 1.2) = 3.2px
при нижнем размере области видимости,40 × (1.2 - 1.2) = 0px
при верхнем размере области видимости.
В качестве базового значения возьмём
120%
, а переменная часть будет от 3,2 до 0 пикселей. Если размер шрифта по умолчанию равен 16 пикселям, то 3,2 пикселя = 0.2rem
, так что множитель равен .2
.Наконец, поскольку переменная часть должна быть равна нулю в верхней точке, нужно инвертировать в формуле контрольные точки:
h1 {
line-height: calc( 120% + 0.2 * 1rem );
}
@media (min-width: 20em) {
h1 {
line-height: calc( 120% + 0.2 * (100vw - 60rem) / (20 - 60) );
}
}
@media (min-width: 60em) {
h1 {
line-height: 120%;
}
}
Два замечания:
- Значение
.2rem
единственно верное, если также имеется шлюз font-size в диапазоне от 24 до 40 пикселей. Здесь это не показано, но есть в исходном коде демки. - Поскольку мы инвертируем значения контрольных точек, обе части дроби
(100vw - 60rem) / (20 - 60)
будут отрицательными для ширин области видимости меньше60em
и больше20em
(включительно). Например, в нижней контрольной точке при размере шрифта по умолчанию 16 пикселей получим-640px / -40
. А если в дроби знаменатель и числитель отрицательные, то результат будет положительным, так что нам не нужно менять знак перед множителем0.2
.
Заключение
Краткие итоги. Мы рассмотрели две формы CSS-шлюзов:
- для свойств, которые могут использовать размерности,
- с примерами для
font-size
иline-height
, - для контрольных точек, измеряемых в пикселях и
em
.
Определяющий фактор — тип контрольной точки. В большинстве проектов вы будете использовать одинаковые точки, скажем, для шлюза
font-size
и для изменений шаблонов. В зависимости от проекта или вашей привычки ключевые точки могут измеряться в пикселях или em
. Лично я предпочитаю пиксели, но оба варианта имеют свои преимущества. Напомню: если вы имеете дело с media-запросами в
em
, то избегайте размерностей в пикселях при определении размеров контейнеров. Также нельзя игнорировать базовый font-size
элемента и можно использовать единственную форму CSS-шлюза:@media (min-width: 20em) and (max-width: 60em) {
selector {
property: calc(
baseline_value +
multiplier *
(100vw - 20rem) / (60 - 20)
);
}
}
Здесь
multiplier
— ожидаемое общее увеличение значения, выраженное в rem
, но без размерности. Например: 0.75 для максимального увеличения на 0.75rem
.Если вы используете media-запросы в пикселях, то вы можете игнорировать базовый
font-size
элемента. Но тогда рекомендую процентные значения. Также можно применять две разные формы CSS-шлюзов. Первая аналогична em/rem
-шлюзу, только со значениями в пикселях:@media (min-width: 320px) and (max-width: 960px) {
selector {
property: calc(
baseline_value +
multiplier *
(100vw - 320px) / (960 - 320)
);
}
}
Здесь
multiplier
— ожидаемое общее увеличение значения, выраженное в пикселях, но без размерности. Например: 12
для максимального увеличения на 12px
.Вторая форма шлюза при вычислении не настолько зависит от браузера. Всё, что можно, мы вычисляем самостоятельно, прежде чем передать эти значения браузеру:
@media (min-width: 320px) and (max-width: 960px) {
selector {
property: calc(
baseline_value + 0.25vw - 10px;
);
}
}
Здесь значения
0.25vw
и -10px
вычислены заранее, вероятно, с помощью Sass или PostCSS.Последняя форма может быть сложнее в реализации (если не использовать mixin), но благодаря более очевидным значениям способна облегчить проверку стилей и отладку.