Математические функции в CSS

Содержание:
min(), max()
clamp()
calc()
Примеры использования
Нюансы
Светлое будущее?
Поддержка браузерами

В CSS есть математические функции, их всего четыре: min(), max(), clamp() и calc().

Математика в каком-то виде есть и в препроцессорах, например, в SCSS есть min(), max() и clamp(), в Less — min() и max(), но есть пара существенных отличий. Во-первых, в препроцессорах всё рассчитывается один раз при компиляции, и в итоговом файле всегда будут фиксированные значения. А во-вторых, в препроцессорных функциях нельзя сочетать разные единицы измерения.

Например, не получится сделать значение на основе пикселей, зависящее от единиц вьюпорта. Вот этот пример работать не будет (это SCSS):

$base-font-size: 16px;

BODY {
  font-size: $base-font-size + 2vw;
}

Компилятор будет ругаться на несочетаемые единицы.

Но если перенести рассчеты в calc(), всё будет работать:

$base-font-size: 16px;

BODY {
  font-size: calc(#{$base-font-size} + 2vw);
}

calc() сможет сложить пиксели и единицы вьюпорта. Это позволит сделать плавающий размер шрифта, который будет зависеть от ширины окна браузера. Препроцессоры так не умеют.

Таким образом, главное преимущество математических функций в CSS — динамичность значений благодаря способности сочетать абсолютные и относительные единицы измерения.

Ещё важно помнить, что при использовании препроцессоров CSS-функции могут конфликтовать с препроцессорными. Например, если в SCSS или в Less написать такое:

width: min(350px, 50%);

компилятор будет ругаться на несовместимые единицы, потому что сработает функция из препроцессора.

Чтобы препроцессоры при компиляции не пытались выполнить CSS-функции, в SCSS предлагается писать имя функции с заглавной буквы:

width: Min(350px, 50%);

Подробнее об этом можно почитать в статье Аны Тюдор When Sass and New CSS Features Collide.

В LESS поможет оборачивание кавычками:

width: ~"min(350px, 50%)";

или компиляция с использованием параметра math со значением strict.

min(), max()

Функция min() возвращает минимальное из переданных значений, max() — максимальное. При использовании процентов или относительных единиц выбираемое значение будет динамическим и будет зависеть от внешних условий. Например:

width: min(350px, 50%);

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

Если вычисленное значение для 50% меньше 350px, ширина блока будет равна 50%. Как только 50% станет больше 350px, функция min() выберет меньшее значение, то есть 350px, и дальше блок тянуться не будет.

Функция min() будет выбирать подходящее значение учитывая ширину окна в данный момент.

Этот код не делает ничего особенного, что не умел бы CSS без математических функций: точно такое же поведение можно получить задав width и max-width:

width: 50%;
max-width: 350px;

Оба варианта для сравнения:

Порастягивайте демо, чтобы увидеть, что разницы в поведении блоков нет.

На первый взгляд, min() и max() не делают ничего интересного, но если подумать, как много мест в CSS, где можно управлять диапазоном значений?

Это доступно только для ширины и высоты:

  • width, min-width, max-width
  • height, min-height, max-height

и их аналогов, привязанных к направлению письма:

  • inline-size, min-inline-size, max-inline-size
  • block-size, min-block-size, max-block-size

У нас нет, например, max-font-size или min-border-width, но благодаря математическим функциям, можно добавить умную динамику практически в любое значение. Например, в размер шрифта:

font-size: max(5vw, 20px);

Поресайзите демо, и вы увидите, что при растягивании шрифт будет бесконечно расти, но при сужении, как только 5vw станет меньше или равным 20px, размер шрифта застынет на этом значении.

Если попытаться реализовать подобное на JS, пришлось бы отслеживать ресайз окна, получать вычисленное значение, и при превышении заданного порога, фиксировать значение на определённом уровне. При использовании min() и max() браузер всё делает сам: отслеживает изменение значений и, при необходимости, фиксирует их — нам нужно просто выбрать функцию и задать диапазон значений.

Это будет работать во всех свойствах, которые используют размеры, в том числе в тенях, градиентах и прочем. Пример с box-shadow:

box-shadow: 0 0 max(45px, 15vw) rgba(0, 0, 0, .5);

Размер тени будет зависеть от размера окна браузера, минимальное значение — 45px.

Экспериментируя с функциями min() и max() можно заметить, что они дают возможность управлять или минимальным значением, или максимальным, но нельзя контролировать и то, и другое одновременно. Хотя совершенно логичным выглядит желание, например, задать размер шрифта, который будет расти в диапазоне от минимального значения до максимального и не выходить за эти значения.

Для такого есть ещё одна функция:

clamp()

Она сочетает в себе min() и max(). Функция получает на вход параметры:

clamp(MIN, VAL, MAX)

И вычисляет значение вот таким образом:

max(MIN, min(VAL, MAX)))

Проще всего её понимать представляя среднее значение (VAL) как желаемое, которое ограничено минимальным и максимальным значениями. Например, этот код

font-size: clamp(20px, 5vw, 40px);

описывает следующее поведение: размер шрифта равен 5vw, но не меньше 20px и не больше 40px. Порастягивайте демо, чтобы увидеть как это работает:

Аналогичный подход часто используется при разработке адаптивных сайтов: мы задаём минимальное и максимальное значения ширины страницы, позволяя ей тянуться и сжиматься в заданных пределах, например, в диапазоне от 320px до 1200px:

min-width: 320px;
max-width: 1200px;

Используя clamp() это можно записать в одну строчку:

width: clamp(320px, 100%, 1200px);

Для следующего демо я взяла значения поменьше, но смысл тот же: блоки тянутся в пределах от 200px до 400px. Ширина верхнего блока управляется с помощью min-width и max-width, нижнего — с помощью clamp():

Оба блока ведут себя совершенно одинаково, разница только в возможностях этих подходов: clamp() позволяет добавить умную динамику значений в любое свойство, не только в размеры блоков. Мне нравится идея использовать clamp() для адаптивных шрифтов, почитать про это можно в статье Linearly Scale font-size with CSS clamp() Based on the Viewport.

Предыдущие функции достаточно просты по сравнению с calc(), самой мощной и интересной.

calc()

Функция позволяет производить математические операции:

  • сложение
  • вычитание
  • умножение
  • деление

В отличие от препроцессорных вычислений, calc() позволяет сочетать любые совместимые значения. Например, можно вычесть пиксели из процентов:

width: calc(100% - 20px);

Препроцессоры так не умеют, потому что на момент компиляции CSS неизвестно чему равны 100%, но у браузера эти данные есть, следовательно, он может взять 100%, перевести их в пиксели и вычесть из них 20px. Кроме того, браузер пересчитает значение, если ширина элемента изменится.

Внутри calc() можно использовать кастомные свойства, и это делает вычисления в CSS гораздо мощнее препроцессорных. Например, на calc() и кастомных свойствах можно без каких-либо скриптов сделать простой генератор цветовых схем:

По клику на цвет в CSS меняется значение CSS-переменной, и вся палитра пересчитывается.

Рассчёт оттенка (hue из HSL) делается таким образом:

--palette-hue-2: calc(var(--palette-hue) + var(--palette-step-1));

И затем оттенок используется для формирования цвета в формате HSL:

background: hsl(var(--palette-hue), var(--base-sat), var(--base-lght));

Конечно, подобные вещи удобнее и логичнее делать на JS, демо просто показывает, что CSS достаточно мощный, чтобы уметь такое.

Примеры использования

Cохранение логики рассчётов

Например, если нужна ширина в 1/12 от общей, можно высчитать значение и записать его в стили:

.block {
  width: 8.33%;
}

Но так непонятно что это за число. Станет немного понятнее, если логику рассчёта положить рядом в комментарии:

.block {
  width: 8.33%; /* 100% / 12 */
}

А если использовать calc(), можно само математическое выражение сделать значением:

.block {
  width: calc(100% / 12);
}

Так сразу понятно, что элемент занимает 1/12 ширины родителя, и не нужно поддерживать комментарии в актуальном состоянии.

Управление размерами элементов

Например, есть карточка с картинкой, картинка ужимается под ширину колонки с текстом:

.card {
  min-width: 300px;
  max-width: 400px;
  padding: 1rem;
}

.card__img {
  width: 100%;
}

Как растянуть картинку на ширину карточки не привязываясь к размерам карточки? С помощью calc():

.card {
  min-width: 300px;
  max-width: 400px;
  padding: 1rem;
}

.card__img {
  /* Ширина картинки */
  width: calc(100% + 2rem);
  /* Сдвиг влево на размер паддинга */
  margin-left: -1rem;
}

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

.card {
  --padding: 1rem;

  min-width: 300px;
  max-width: 400px;
  padding: var(--padding);
}

.card__img {
  width: calc(100% + var(--padding) * 2);
  margin-left: calc(var(--padding) * -1);
}

Так сохранится логика рассчётов, 1rem становится не магическим числом, а именованной переменной, и по самому коду будет понятно, что ширина картинки увеличивается на два паддинга, а потом сдвигается влево на один. Можно примерно понять что делает этот код даже не глядя на страницу в браузере.

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

.input {
  width: calc(100% - 40px);
}

Текстовое поле всегда будет отодвинуто от края на 40px (+ паддинг) независимо от размера родительского элемента.

Адаптивная типографика

С помощью calc() можно примешивать единицы вьюпорта к обычному размеру шрифта, тогда при увеличении размеров экрана будет расти и шрифт:

BODY {
  font-size: calc(1rem + .5vw);
  line-height: calc(1.2rem + .5vw);
}

Порастягивайте демо, чтобы увидеть как размер окна влияет на размер шрифта:

Почитать об этом больше можно тут или тут.

Мне больше нравится вариант с clamp(), который не только позволяет шрифту расти, но и задаёт ему верхнюю и нижнюю границы.

Управление размерами фона

С помощью calc() можно задать размер фоновому изображению, комбинируя фиксированные единицы и проценты. Например, чтобы фон всегда отступал от краёв на определённое значение:

DIV {
  --offset: 1rem;
  --bg-size: calc(100% - var(--offset) * 2);

  /* Цветной градиент */
  background: linear-gradient(45deg, crimson, 20%, gold, 80%, turquoise) no-repeat;
  /* Размер фона */
  background-size: var(--bg-size) var(--bg-size);
  /* Отступ от края элемента */
  background-position: var(--offset) var(--offset);
}

Полосатые поля показывают прозрачные области вокруг разноцветного градиента.

Используя этот же подход можно делать вырезанные углы квадратной формы:

DIV {
  --offset: 1rem;
  --bg-size: calc(100% - var(--offset) * 2);

  /* Однотонный фон из двух градиентов */
  background:
    linear-gradient(turquoise, turquoise),
    linear-gradient(turquoise, turquoise);
  background-repeat: no-repeat;
  /* Один фон сжат по вертикали, другой по горизонтали */
  background-size:
    100% var(--bg-size),
    var(--bg-size) 100%;
  /* Один фон сдвинут по вертикали, другой по горизонтали */
  background-position:
    0 var(--offset),
    var(--offset) 0;
}

В отличие от версии с коническими градиентами, этот вариант будет работать во всех браузерах. Способ был предложен Ильёй Стрельциным.

Также можно рисовать линейными градиентами полосы заданной ширины:

DIV {
  --line-witdh: 3rem;

  background:
    linear-gradient(
      to left top,
      transparent calc(50% - var(--line-witdh) / 2),
      turquoise 0, turquoise calc(50% + var(--line-witdh) / 2),
      transparent 0
    );
}

Вычисление цветов и шагов градиента

Иногда для экспериментов нужен полосатый градиент. Чтобы не считать параметры руками, их можно вычислять с помощью calc() на основе кастомных свойств. Вот так задаются параметры:

--steps: 9;
--hue-step: calc(360deg / var(--steps));
--line-width: 1rem;

А вот так потом высчитывается оттенок:

hsl(calc(var(--hue-step) * 2), 100%, 63%)

И точка остановки:

calc(var(--line-width) * 7)

Редактируя переменные можно менять параметры градиента без необходимости пересчитывать вручную шаги или переписывать весь градиент.

Правда, при таком автоматическим вычислении оттенков могут получаться не очень красивые цвета, но для экспериментов вполне пойдёт.

Область применения cacl() гораздо шире перечисленных примеров. В некоторых случаях, если значения не должны меняться динамически, с подобными задачами справятся и препроцессоры, но если должны, например, при изменении кастомных свойств или размера окна, — без calc() не обойтись.

Нюансы

При использовании calc() нужно помнить о некоторых тонкостях:

  • арифметический знак всегда нужно окружать пробелами, чтобы браузер мог правильно разобрать выражение. Например, width: calc(100% -50px) не будет работать, правильно так: width: calc(100% - 50px). Для умножения и деления это требование не является обязательным, но лучше всегда добавлять пробелы, чтобы сохранить код единообразным и легко читаемым;
  • делить на ноль нельзя, в результате будет ошибка;
  • calc() умеет производить рассчёты только для совместимых единиц. Например, можно сложить пиксели с процентами или единицами вьюпорта, градусы с радианами и вычесть миллисекунды из секунд, но вот сложить секунды с пикселями или градусы с единицами вьюпорта ожидаемо не получится, потому что непонятно в каких единицах ожидается результат.

    У меня была слегка безумная идея привязать градусы градиента к ширине вьюпорта (и вращать градиент растягивая окно браузера), но я не придумала как это можно реализовать, и не уверена, что это в принципе возможно без JS;

  • рассчёты в медиавыражениях поддерживаются только для одинаковых единиц: пиксели можно складывать с пикселями, ремы с ремами. Складывать пиксели с ремами внутри медиавыражения нельзя. Кастомные свойства внутри медиавыражений не работают вообще, и никакие вычисления с ними, соответственно, невозможны (демо с calc() в @media). Для каких-то сложных конструкций можно попробовать воспользоваться логическими операциями.

Светлое будущее?

В спецификации есть интересный момент: утверждается, что внутри calc() в качестве значений можно использовать содержимое атрибутов, да и в принципе можно использовать атрибуты как значения CSS-свойств.

От обычного использования attr() в качестве содержимого псевдоэелемента это отличается указанием типа содержимого:

/* HTML
<div data-color="#FC9">...</div>
*/

background: attr(data-color color, orange);

Или единиц измерения:

/* HTML
<div data-length="300">...</div>
*/

width: attr(data-length px, 200px);

Значение после запятой — запасное, на случай, если не удастся получить или распарсить значение атрибута.

Поначалу эта конструкция показалась мне странной: зачем класть данные в атрибуты, а потом ещё и типизировать их в CSS, если можно сразу положить нужное в кастомные свойства?

Например:

/* HTML
<div style="--width: 100px"></div>
*/

DIV {
  width: var(--width);
  height: 50px;
  background: gold;
}

Но в твиттере мне быстро накидали примеров, когда это было бы действительно удобно. Например, получая размеры из атрибутов было бы удобно делать адаптивные картинки и фреймы. У меня получилось сделать адаптивное видео, используя кастомные свойства, но для этого приходится дублировать размеры в инлайновых стилях. Если бы можно было считывать значения из атрибутов, без дублирования можно было бы обойтись.

На момент написания статьи это не поддерживается ни одним браузером, но есть некоторая надежда, что однажды это заработает, потому что подобное использование атрибутов описано в спецификации свойства, которое помогло бы управлять соотношением сторон: aspect-ratio. Там есть такой пример:

/* HTML
<iframe
  src="https://www.youtube.com/embed/0Gr1XSyxZy0"
  width=560
  height=315>
*/

@supports (aspect-ratio: attr(width number) / 1) {
  iframe {
    aspect-ratio: attr(width number) / attr(height number);
    width: 100%;
    height: auto;
  }
}

И вот это было бы очень круто, потому что избавило бы разработчиков от необходимости городить странные конструкции для решения той же задачи. Почитать про aspect-ratio можно в статье Рэйчел Эндрю Designing An Aspect Ratio Unit For CSS.

Также не так давно в черновики были добавлены другие математические функции, например, mod(), round(), sin() и многие другие. Это совсем свежее добавление, новые функции ещё нигде не поддерживаются.

Поддержка браузерами

min(), max(), clamp() и calc() поддерживаются в большинстве современных браузеров, их поддержка примерно совпадает с кастомными свойствами. И то, и другое не работает в IE11.

При необходимости проверить поддержку функций и кастомных свойств можно воспользоваться @supports (также учитывая поддержку браузерами для него):

@supports (--color: pink) {
  ...
}

@supports (width: min(10px, 20px)) {
  ...
}
Ссылки по теме:
Mathematical Expressions: calc(), min(), max() and clamp()
Less: Math Functions
Less: Escaping
Sass:math
Special Functions
calc()
Getting Started With CSS calc ()
Когда бывает нужен calc()
Keep Math in the CSS
When Sass and New CSS Features Collide
Hardcore CSS calc()
A Complete Guide to calc() in CSS
The Flexbox Holy Albatross Reincarnated
Fluid typography examples
Fluid Typography
Fun with Viewport Units
Logic in Media Queries
aspect-ratio
Designing An Aspect Ratio Unit For CSS
Fluid Width Video
Linearly Scale font-size with CSS clamp() Based on the Viewport]()
Attribute References: attr()
attr()
Новые математические функции в модуле единиц и значений CSS 4 уровня – еще один шаг к полноценному программированию на CSS
@supports
Единицы размеров в CSS
Адаптивное видео с помощью встроенных математических функций CSS
Адаптивное видео с помощью встроенных математических функций CSS
Наверх