Математика CSS-шлюзов
Перевод статьи The math of CSS locks с сайта fvsch.com для CSS-live.ru, автор — Флоран Вершельд
CSS-шлюз — приём в отзывчивом дизайне, позволяющий сделать плавный переход между двумя значениями, в зависимости от текущего размера окна браузера, вместо резкого «перескока» с одного значения на другое.
Эту идею, вместе с одной из реализаций, предложил Тим Браун в статье «Гибкая типографика с помощью CSS-шлюзов»
Когда я вникал в реализацию Тима и пробовал свои варианты, мне было нелегко разобраться, что именно там происходит. По ходу дела я набросал кучу формул, и решил, что будет полезно поделиться пояснением математической части.
Я опишу сам приём, его ограничения, и математику, на которой он строится. Но не пугайтесь насчет математики: по сути это сложение и умножение, к тому же я расписал всё по шагам подробнее некуда, а еще будут симпатичные графики.
Оглавление
- Что такое CSS-шлюз?
- CSS-шлюзы с контрольными точками в пикселях
- CSS-шлюзы с контрольными точками в em-ах
- Заключение
Что такое CSS-шлюз?
Размеры относительно окна браузера
В последнем проекте у меня был баннер во всю ширину с заголовком сверху, причем макет был только для настольных ПК, с крупным шрифтом. Я решил, что мне нужен шрифт помельче для маленьких экранов и что-то промежуточное для средних. Почему бы не сделать шрифт зависимым от ширины окна?
Ранние подходы для этого выглядели примерно так:
h1 { font-size: 4vw; /* Бац! Готово. */ }
У этого было два недостатка:
- Текст становится совсем мелким на маленьких экранах (12.8 пикселей при 320px) и очень крупным на больших (64px при 1600px);
- Он не реагирует на пользовательские настройки для размера шрифта.
Техники CSS-шлюзов призваны исправить первый пункт. Отличные техники CSS-шлюзов стараются исправить и второй: учет пользовательских предпочтений.
Понятие CSS-шлюза
CSS-шлюз — такой способ вычисления CSS-значения, при котором:
- есть минимальное и максимальное значения,
- есть две контрольные точки (обычно отталкивающиеся от ширины окна)
- и между этими контрольными точками фактическое значение линейно меняется от минимума до максимума.

«Пусть у нас font-size будет 20px при 320px и меньше, 40px при 960px и выше, а в промежутке его значение меняется от 20px до 40px».
В CSS это может выглядеть так:
h1 { font-size: 1.25rem; }
@media (min-width: 320px) {
h1 { font-size: /* магическое значение от 1.25rem до 2.5rem */; }
}
@media (min-width: 960px) {
h1 { font-size: 2.5rem; }
}
Первой трудностью для нас будет реализовать само это магическое значение. Не стану вас интриговать и сразу покажу отгадку, что оно будет выглядеть примерно так:
h1 {
font-size: calc(1.25rem + значение_относительно_окна);
}
Где значение_относительно_окна может быть отдельным значением (напр., 3vw) или более сложным выражением (тоже на основе единицы vw или другой единицы области просмотра).
Ограничения
Раз CSS-шлюзы строятся на базе единиц области просмотра, у них есть важные ограничения. Они работают только для числовых значений, могут использовать calc() и принимают значения в пикселях.
Почему в пикселях? Потому что единицы области просмотра (vw, vh, vmin и vmax) всегда в конечном итоге переводятся в пиксели. Например, при ширине окна 768px 1vw переводится в 7.68px.
(У Тима в статье есть ошибка, где он пишет, что выражение типа 100vw - 30em переводится в em. Это не так: браузер воспримет 100vw как значение в пикселях, и вычтет из него столько пикселей, сколько их окажется в 30em для этого элемента и этого свойства.)
Несколько примеров того, что работать не будет:
- CSS-шлюз для свойства opacity, потому что
opacity: calc(.5+1px)— это ошибка; - CSS-шлюз для большинства функций
transform(напр.,rotate:нельзя повернуть что-либо на столько-то пикселей)
Что ж, на первый взгляд с одними лишь пикселями особо не разгуляешься, но, может быть, кто-то возьмет на себя смелость отыскать все свойства и приемы, где от CSS-шлюзов может быть польза.
Для разминки, возьмем свойства font-size и line-heightи посмотрим, как построить для них CSS-шлюзы с контрольными точками как в пикселях, так и в em-ах.
CSS-шлюзы с контрольными точками в пикселях
Примеры
- CSS-шлюз с calc для font-size (rem+px, медиавыражение в px)
- CSS-шлюз с calc для line-height (%+px, медиавыражение в px)
- Объединенный шлюз для font-size и line-height (на основе px)
В ближайших подразделах мы покажем, как мы получили CSS-код для каждого из примеров.
Размер шрифта как линейная функция
Нам нужно, чтобы font-size пропорционально увеличивался между двумя точками: 20px при 320px и 40px при 960px. Мы можем отметить наши две точки на графике и провести через них линию:

То, что здесь выделено красным — простая линейная функция. Можно записать ее в виде y = mx + b, где:
y— наш размер шрифта (вертикальная ось)x— ширина области просмотра, в пикселях (горизонтальная ось)m— крутизна наклона функции («сколько пикселей прибавляется к размеру шрифта на каждый пиксель увеличения ширины окна?»),- и
b— размер шрифта до того, как к нему добавится какое-либо относительное значение, связанное с размером окна.
Наша задача — выяснить значения m и b соответственно. Они — неизменные части этого уравнения.
Сначала найдем значение m. Для этого нам достаточно двух опорных точек (x,y). Это похоже на то, как рассчитывается скорость (расстояние от времени), только здесь это font-size от ширины окна:
m = приращение_размера_шрифта / приращение_области_просмотра
m = font_size_increase / viewport_increase m = (y2 - y1) / (x2 - x1) m = (40 - 20) / (960 - 320) m = 20 / 640 m = 0.03125
Этот же расчет можно представить по-другому:
- Полное приращение font-size равно 20px (40 — 20).
- Полное приращение ширины окна равно 640px (960 — 320).
- Если ширину окна увеличить всего на 1px, на какую величину увеличится font-size? На 20 / 640 = 0.03125px.
Теперь вычислим 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
Стоит заметить, что это значение 10px можно было найти, просто взглянув на график. Но не всегда у нас есть график под рукой:)
Как бы то ни было, у нас получилась вот такая линейная функция:
y = 0.03125x + 10
Преобразуем в CSS
Как нам преобразовать нашу функцию в синтаксис CSS? Мы знаем, что y — это font-size, и что для основных арифметических действий в CSS нам понадобится calc().
font-size: calc( 0.03125x + 10px );
Не так уж плохо. Конечно, это не настоящий CSS, поскольку x не является валидным CSS-синтаксисом. Но для нашей линейной функции x представляет собой ширину окна, которую в CSS можно обозначить как 100vw.
font-size: calc( 0.03125 * 100vw + 10px );
Вот это уже работающий CSS. Если хочется стиля покороче, можно сократить это умножение. Поскольку 0.03125 × 100 = 3.125:
font-size: calc( 3.125vw + 10px );
Разумеется, мы хотим применять этот стиль только для размеров окна от 320px до 960px. Так что добавим пару медиавыражений:
h1 { font-size: 20px; }
@media (min-width: 320px) {
h1 { font-size: calc( 3.125vw + 10px ); }
}
@media (min-width: 960px) {
h1 { font-size: 40px; }
}
И теперь график у нас выглядит как тот, что показан во введении.

Это славно, но меня не очень радуют эти пиксельные значения для размера шрифта: нельзя ли сделать лучше?
Учитываем пользовательские предпочтения
Практически в любом браузере пользователь может выбрать более мелкий или более крупный текст по умолчанию. Типичное значение по умолчанию — 16px, но пользователи могут поменять его на что угодно (как правило, на более крупный шрифт).
Я хотел бы ввести это пользовательское предпочтение в нашу формулу, и собираюсь для этого воспользоваться значениями в rem. Обратите внимание, что те же принципы подходят и к значениям в em или процентах.
Первым делом надо убедиться, что font-size корневого элемента не переопределен абсолютным значением. Например, если вы используете CSS из Bootstrap 3, то там есть примерно такой кусочек кода:
html {
font-size: 10px;
}
Никогда так не делайте! (К счастью, это исправлено в Bootstrap 4). Если вам действительно для чего-то понадобится переопределить значение em корневого элемента(1rem), можете использовать такое:
/*
* Переопределяем значение rem, сохраняя его пропорциональным.
* Полезные значения, с font-size 16px по умолчанию:
* • 62.5% -> 1rem = 10px, .1rem = 1px
* • 125% -> 1rem = 20px, .05rem = 1px
*/
html {
font-size: 62.5%;
}
Учитывая это, мы собираемся вообще не трогать font-size корневого элемента, так что по умолчанию он будет равен 16px. Посмотрим, что получится, если в нашем шлюзе для 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; }
}
Если мы запустим этот код с настройками браузера по умолчанию, мы увидим, что он ведёт себя как наш предыдущий код с пикселями. Здорово!
Но раз мы делаем это ради поддержки пользовательских изменений, надо проверить в действии и это. Скажем, наш пользователь настроил свой браузер так, что font-size по умолчанию у него не 16px, а 24px (увеличен на 50%): как отреагирует вышеприведенный код? Давайте построим график:

Пунктирная синяя линия: с базовым font-size в 16px.
Сплошная красная линия: с базовым font-size в 24px.
В контрольной точке 320px размер шрифта оказывается меньше (перескакивая с 30px на 25px), и есть большой перескок в верхней контрольной точке (с 45px на 60px). Ой.
Чтобы это исправить, можно использовать одно и то же базовое значение, настраиваемое пользователем, для всех 3 размеров. Например, можем взять базовое значение 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′. В нашем случае, раз мы знаем, что наше базовое значение эквивалентно 20px, можно получить значение b′ простым вычитанием:
b′ = b - baseline_value b′ = 10 - 20 b′ = 10
Другая стратегия — выбрать базовое значение изначально, до всего остального, а затем находить линейную функцию, которая описывает увеличение font-size (я буду называть ее y′ , чтобы не путать с y, обозначающей весь font-size). Давайте быстренько попробуем:
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, и выглядит это так:

С нашим базовым значением в rem и дополнительными размерами в vw и/или px мы в итоге получили полностью работающий шлюз для font-size. Когда пользователь меняет свой базовый font-size, вся конструкция поднимается или опускается и при этом не ломается. Получилось!

Штриховая пурпурная линия: только увеличение font-size.
Пунктирная синяя линия: с базовым font-size в 16px.
Сплошная красная линия: с базовым font-size в 24px.
Конечно, это не совсем то, чего хотел пользователь: требовался шрифт на 50% больше, а у нас шрифт получается на 50% больше на маленьких экранах, но только на 25% больше на больших. Но всё равно это неплохой компромисс.
Делаем шлюз для line-height
В этой части мы будем рассматривать такой сценарий: «нам нужно, чтобы line-height у абзацев был равен 140% при 320px и 180% при 960px».
Поскольку мы будем работать с базовым значением с динамическим значением в пикселях в придачу, нам нужно знать, скольким пикселям соответствуют эти дроби 1.4 и 1.8. А значит, нам понадобится знать font-size наших абзацев. Допустим, у наших абзацев font-size по умолчанию, т.е., скорее всего, 16px. Опорные точки у нас такие:
16 * 1.4 = 22.4пикселя в нижней контрольной точке (320px)16 * 1.8 = 28.8пикселя в верхней контрольной точке (960px)
Также мы возьмем 140% = 22.4px в качестве базового значения. Так что нужная нам отсюда информация — приращение на 6.4px. Можно построить линейную формулу, как раньше:
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().
Затем мы добавим медиавыражения и убедимся, что все объявления для значения 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%, результат будет правильным для базового размера шрифта в 16px, но испортится, если пользователь его изменит.
Можно построить график нашей функции и проверить, что она работает с разными базовыми font-size.

Пунктирная синяя линия: с базовым font-size в 16px.
Сплошная красная линия: с базовым font-size в 24px.
Наконец, поскольку наша формула для line-height зависит от собственного font-size элемента, то при изменении размера его шрифта нам придется изменить и формулу. Например, в демо для line-height у нас есть абзац с более крупным текстом, определенный вот так:
.big {
font-size: 166%;
}
Это меняет наши опорные точки:
16 * 1.66 * 1.4 = 37.184пикселя в нижней контрольной точке (320px)16 * 1.66 * 1.8 = 47.808пикселя в верхней контрольной точке (60px)
Можно провести вычисления и получить вот такую доработанную формулу: 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-height-ы, что и для стандартных абзацев, нам просто нужно добавить множитель 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
Хорошо, давайте попробуем собрать это всё вместе. Вот наш сценарий: у нас есть резиновая колонка текста с H1 и несколькими абзацами, и мы собираемся изменить font-size и line-height, используя следующие значения:
| Элемент и свойство | Значение при 320px | Значение при 960px |
|---|---|---|
| H1 font-size | 24px | 40px |
| H1 line-height | 133.33% | 120% |
| P font-size | 15px | 18px |
| P line-height | 150% | 166.67% |
Вы наверняка заметите, что мы производим два разных действия над высотой строк. Как правило, когда шрифт увеличивается, line-height следует уплотнять, а когда колонка расширяется, line-height принято увеличивать, чтобы строки стали пореже. Но в нашем случае это происходит одновременно, и эти два принципа противоречат друг другу! Поэтому нам придется выбирать, какой из аспектов важнее:
- Для H1, на наш взгляд, увеличение font-size будет более заметным, чем увеличение ширины колонки.
- Для абзацев, как нам кажется, увеличение ширины колонки будет существеннее, чем едва заметное увеличение font-size.
Теперь давайте выберем две контрольные точки. Я снова возьму 320px и 960px, ура. Начнем с того, что запишем шлюзы для 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 to 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%, что можно округлить до 32px.
Нам нужна линейная функция, описывающая «то, что мы добавим к базовому значению 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 );
}
Давайте взглянем на нашу функцию на графике, и посмотрим, как она соотносится с соответствующей функцией font-size.

Пунктирная синяя линия: уменьшение line-height
Штриховая красная линия: наше базовое значение line-height (120% от font-size заголовка).
Сплошная пурпурная линия: итоговый line-height.
На этом графике мы видим, что итоговый line-height (пурпурная линия) равен 120% базового значения (красные штрихи) плюс увеличение line-height (синий пунктир). Вы можете посмотреть на эти уравнения на GraphSketch.com и проверить сами.
Для абзацев мы будем использовать 150% в качестве нашего базового значения. Увеличение line-height, которое нам нужно, вот такое: (1.75 - 1.5) × 18 = 4.5px.

Мой калькулятор выдает мне следующую формулу: 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) );
}
}
Занятное совпадение, что такую же формулу использовал Тим Браун в своей «Гибкой типографике с помощью CSS-шлюзов», только у нас в переменной части пиксели, а не 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. К сожалению, сейчас у меня нет готового примера, но если вы сами готовы взяться за это, дайте мне знать, я добавлю ваш пример в статью.
CSS-шлюзы с контрольными точками в em-ах
Обновленные примеры
Я взял три наших первых примера и переделал их так, что вместо пикселей для контрольных точек и приращений значения теперь для контрольных точек используются em-ы, а для приращений значения — rem-ы.
- CSS-шлюз с calc для font-size (rem+rem, медиавыражение в em)
- CSS-шлюз с calc для line-height (%+rem, медиавыражение в px)
- Комбинированный шлюз font-size и line-height (основанный на em/rem)
В следующих подразделах мы опишем, как именно работает синтаксис, использованный для этих примеров.
Не используйте медиавыражения в em-ах при m × 100vw
Помните синтаксис m × 100vw, который мы использовали во втором разделе (к примеру, в коде наподобие calc(base + 2.5vw))? Это нельзя использовать с медиавыражениями, основанными на em.
Дело в том, что в случае медиавыражений обе единицы em и rem обозначают одно и то же: базовый размер шрифта браузера. Который — как мы уже много раз отмечали — обычно равен 16px, но может быть меньше или больше в зависимости от двух вещей:
- Выбора браузера или ОС (в основном для особых случаев вроде браузеров в телевизорах и некоторых электронных книг).
- Предпочтения пользователя.
Это значит, что, если у нас есть две контрольные точки в 20em и 60em, фактически они будут соответствовать следующим ширинам в CSS:
- 320px и 960px для базового размера шрифта 16px.
- 480px и1440px для базового размера шрифта 24px.
- и т.д.
(Заметьте, что это CSS-пиксели, а не пиксели устройства. В этой статье нас не интересуют пиксели устройства, поскольку они не влияют на наши вычисления.)
Во втором разделе у нас были примеры такого вида:
font-size: calc( 3.125vw + .625rem );
Если мы возьмём этот синтаксис и переделаем все контрольные точки на em-ы, предполагая, что 1em в медиавыражениях равен 16px, то у нас получится код вроде этого:
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 ); }
}
Это, конечно, работало бы, если бы операционная система, браузер и пользователь никогда не меняли базовый размер шрифта. Но едва он по какой-либо причине окажется другим, как тут же начнется хаос.

Пунктирная синяя линия: результат при базовом размере шрифта 16px.
Сплошная красная линия: результат при базовом размере шрифта 24px
Что тут происходит? Когда мы меняем базовый font-size, наши контрольные точки в em-ах смещаются в сторону более высоких значений в пикселях. Но наше значение 3.125vw — 10px верно только для конкретных контрольных точек в пикселях!
- При 320px, 3.125vw — 10px равно 0px, как запланировано.
- Но при 480px, 3.125vw — 10px равно 5px.
В верхней контрольной точке всё ещё хуже:
- При 960px, 3.125vw — 10px равно 20px, как ожидается.
- При 1440px, 3.125vw — 10px равно 35px (на 15px больше, чем нужно).
Если мы хотим использовать контрольные точки в em-ах, нам понадобится другой подход.
Еще раз посчитаем
Этот прием, показанный в статье Тима Брауна, основан на том, что CSS может взять на себя основную часть расчетов, используя две переменные части:
- 100vw, ширина области просмотра;
- нижняя контрольная точка, выраженная в rem.
Формула, которую мы будем использовать:
y = m × (x - x1) / (x2 - x1)
Как мы пришли к этой формуле? Давайте вернемся на несколько шагов назад. Во втором разделе мы показали, что наши font-size и line-height можно описать в виде линейной функции:
y = mx + b
В CSS мы можем работать с x (это 100vw). Но мы не можем вычислить m и b как точные значения в px или 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′ = максимальное_приращение_значения × (x - x1) / (x2 - x1)
Преобразуем в CSS
Теперь мы можем использовать это значение в CSS. Возвращаясь к нашему примеру «от 20px до 40px», мы можем записать его как:
@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 равен 16px, а ширина области просмотра — 600px, то результат будет 280px (600 - 20 × 16). Если базовый font-size равен 24px а ширина области просмотра — 600px, то результат будет 120px (600 - 20 × 24).

Заметьте, что мы выражаем наши контрольные точки в единицах rem. Почему не em, спросите вы? Потому что в CSS-значениях em соответствует не базовому font-size, а font-size самого элемента (обычно) или его родительскому font-size (когда используется в самом свойстве font-size).
В идеале, нам нужна была бы единица CSS, которая ссылается на базовый font-size браузера, но такой единицы не существует. Самое близкое к этому, что у нас есть — это 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.
Например, при базовом font-size в 16px и ширине окна 600px мы получили бы:
n = (x - x1) / (x2 - x1) n = (600 - 320) / (960 - 320) n = 280 / 640 n = 0.475
К сожалению, это так не работает.
Главная причина в том, что нельзя использовать пиксели, и вообще единицы измерения CSS, в делителе при делении в calc(). (Делитель — это то, что справа. Не беда, если вы этого не помните из школьных знаний, мне самому вот пришлось заглянуть в справочник.) Делить можно только на безразмерную величину. Какие же у нас тут варианты?
Что, если просто удалить единицы измерения в делителе? Что будет в результате calc((100vw - 20rem)/(60 - 20))?
| Ширина окна | Деление в 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 |
| Ширина окна | Деление в 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. Этим можно воспользоваться!
На очереди у нас множитель 20px, который мы использовали в первой попытке заставить этот CSS работать. Нужно будет избавиться от него.
В первой попытке мы пытались добиться примерно такого кода:
font-size: calc( 1.25rem + 20px * n );
Где n предполагалось значением от 0 до 1. Но из-за ограничений синтаксиса для деления в calc() мы не могли получить тот результат от 0 до 1, что нам нужен.
Нам удалось получить лишь пиксельное значение, эквивалентное 0rem и 1rem; давайте назовем это значение r.
Еще одно ограничение затрагивает умножение в calc(). В записи calc(a * b) либо a, либо b должно быть безразмерным числом.
Поскольку у r уже есть единица измерения (это пиксельное значение), другой множитель должен быть безразмерным.
В нашем примере требуется 20-пиксельное увеличение в верхней контрольной точке. 20px — это 1.25rem, поэтому мы будем использовать множитель 1.25:
font-size: calc( 1.25rem + 1.25 * r );
Это должно работать как надо, но заметьте, что значение r будет меняться в зависимости от базового font-size.
- При базовом значении 16px, 1.25 * r будет значением между 0px и 20px.
- При базовом значении 24px, 1.25 * r будет значением между 0px и 30px.
Давайте напишем весь CSS-шлюз, с медиавыражениями, верхними значениями и нижними:
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 на основе px, в этот раз, когда пользователь увеличивает базовый font-size на 50%, всё увеличивается на 50%: базовое значение, переменная часть и контрольные точки. Мы получим диапазон 30px–60px вместо диапазона 20px–40px по умолчанию.

Пунктирная синяя линия: результат при базовом размере шрифта 16px.
Сплошная красная линия: результат при базовом размере шрифта 24px.
Можете проверить это поведение в нашем первом примере с em-ами.
Шлюзы для line-height с em/rem
Во втором примере мы хотим менять line-height абзаца между 140% и 180%. Мы используем 140% в качестве базового значения, а для переменной части используем ту же формулу, что в примере с the 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, увеличение line-height на 40%, которое мы ищем, соответствует .4rem. Это и будет тем значением, которое мы используем в наших двух выражениях calc().
Теперь посмотрим на пример с line-height из нашего третьего демо. Мы хотим, чтобы значение line-height у H1 уменьшалось от 133.33% до 120%. Мы также знаем, что одновременно будет меняться его font-size.
Для этого самого примера во втором разделе мы выяснили, что это уменьшение line-height можно выразить через две опорные точки:
24 × (1.3333 - 1.2) = 3.2pxв нижней контрольной точке,40 × (1.2 - 1.2) = 0pxв верхней контрольной точке.
Поэтому используем базовое значение 120% и переменную часть от 3.2px до 0px. При базовом размере шрифта 16px 3.2px равно 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, меняющий значение от 24px до 40px (это не показано здесь, но видно в исходнике примера). - Поскольку мы меняем местами значения контрольных точек, при любой ширине окна в пределах от
20emвключительно до60emи делимое, и делитель в выражении(100vw - 60rem) / (20 - 60)будут отрицательными. Например, в нижней контрольной точке и при базовом размере шрифта 16px оно равно640px / -40. И так как минус, деленный на минус, дает плюс, нам не нужно менять знак перед множителем0.2.
Заключение
Краткие итоги нашего исследования. Мы показали два варианта CSS-шлюзов:
- для свойств, которые могут использовать размеры,
- с примерами font-size и line-height
- и для контрольных точек как как в пикселях, так и в ем-ах
Контрольные точки какого типа вы используете — это главное условие. В веб-проектах чаще всего бывает нужно использовать одни и те же контрольные точки и для, скажем, шлюзов с font-size, и для смены раскладки. В зависимости от проекта или стиля кодинга у ваших контрольных точек могут быть значения в пикселях или em-ах. (Я предпочитаю контрольные точки в пикселях, но свои преимущества есть у обоих вариантов. На всякий случай напомню, что если вы используете медиавыражения, основанные на em, то вам нужно избегать пиксельных значений при задании размеров контейнерам.)
С медиавыражениями на базе em не стоит переопределять font-size корневого элемента, и можно использовать только одну форму CSS-шлюзов:
@media (min-width: 20em) and (max-width: 60em) {
selector {
property: calc(
базовое_значение+
множитель *
(100vw - 20rem) / (60 - 20)
);
}
}
Где множитель — это ожидаемое полное увеличение значения в rem, без единицы измерения (например: 0.75 для максимального увеличения 0.75rem).
С медиавыражениями в пикселях можно переопределять font-size конреного элемента (хотя, если так делать, я рекомендую задавать значение в процентах), и вы можете использовать две разных формы CSS-шлюзов. Первая форма аналогична шлюзу с em/rem, но с пиксельными значениями:
@media (min-width: 320px) and (max-width: 960px) {
selector {
property: calc(
базовое_значение+
множитель *
(100vw - 320px) / (960 - 320)
);
}
}
Где множитель — это ожидаемое полное увеличение значения в px, без единиц измерения (например: 12 для максимального увеличения 12px).
Эта вторая форма также меньше полагается в решении уравнения на сам браузер; вместо это мы сами рассчитываем всё, что можем, до того, как отдать браузеру значения.
@media (min-width: 320px) and (max-width: 960px) {
selector {
property: calc(
базовое_значение + 0.25vw - 10px;
);
}
}
Где значения 0.25vw и -10px рассчитываются заранее, возможно, с помощью миксина Sass или PostCSS.
Понять эту последнюю форму может быть чуть труднее (если мы не пользуемся хорошим миксином), но она может облегчить анализ и отладку стилей — поскольку с ней сразу ясно, какие значения мы добавляем.
P.S. Это тоже может быть интересно:
Очень познавательная статья. Все подробно расписано. Спасибо за перевод!
ЗЫ: еще я понял зачем нужно было учить математику в школе )
Вот у Вас в анналах, чего только не нароешь! Интересная идея! Благодарю за перевод :)
Вот у Вас в анналах, чего только не нароешь! Интересная идея! Благодарю за перевод :)
Автор, ты предлагаешь над каждым проектом выводить эти графики-нафики?!
Переводчик вряд ли сможет ответить за автора (оригинала), лучше спросить у него самого (в начале статьи есть ссылка на его твиттер). Но вообще графики тут служат только иллюстрациями к формулам, чтобы было понятнее, что в них происходит. Когда уже ясно, что куда подставлять (и почему), графики больше не нужны:)
Решение в реализации может быть и проще? если использовать любую программу которая может построить график на основе таблицы(Excel и т.п.) с данными(размер viewport соответствующее значение к примеру шрифта) и вывести линейную линию тренда(это к слову о сложностях расчета). В результате мы увидим какое то выражение, к примеру y = 0,0045x + 12,571. Осталось только перевести его в CSS. Y — в данном примере шрифт(данные из реального проекта), значение на viewport от 768 до 320px. X — значение 100vw(ширина viewport текущая ). Первое действие 0,0045x сразу вычисляем(благо умножить на 100 не трудно) — 0.45vw(не забыть единицы измерения). В итоге у нас получается вот такой результат: «font-size: calc(0.45vw + 12.571px)». Подставим значения, viewport — 320px: 1vw=(320px/100) = 3.2px, а у нас 0,42vw то есть 3.2 * 0.45 = 1.44px добавим коэффициент из формулы 1.44 + 12.571 = 14.011px(сотки после запятой результаты округлений). Данный формула линейно меняла шрифт от 14 на 320px до 16 — 768px.
Удобный сервис для получения готового значения в CSS
https://modern-fluid-typography.vercel.app/