CSS: полное руководство по функции calc()

0
588

CSS: полное руководство по функции calc()

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

.main-content {
  /* Вычесть 80px из 100vh */
  height: calc(100vh - 80px);
}

Здесь с CSS-кодом, в котором используется calc(), можно поэкспериментировать.

CSS: полное руководство по функции calc()
CSS: полное руководство по функции calc()

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

Функция calc() и значения CSS-свойств

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

.el {
  font-size: calc(3vw + 2px);
  width:     calc(100% - 20px);
  height:    calc(100vh - 20px);
  padding:   calc(1vw + 5px);
}

Функцию calc() можно применять и для установки любой отдельной части свойства:

.el {
  margin: 10px calc(2vw + 5px);
  border-radius: 15px calc(15px / 3) 4px 2px;
  transition: transform calc(1s - 120ms);
}

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

.el {
  background: #1E88E5 linear-gradient(
    to bottom,
    #1E88E5,
    #1E88E5 calc(50% - 10px),
    #3949AB calc(50% + 10px),
    #3949AB
  );
}

Функция calc() — это средство для работы с числовыми свойствами

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

.el {
  /* Неправильно! */
  counter-reset: calc("My " + "counter");
}
.el::before {
  /* Неправильно! */
  content: calc("Candyman " * 3);
}

Существует множество единиц измерения, которые можно применять в CSS для указания размеров элементов и их частей: px%emreminmmcmptpcexchvhvwvminvmax. Все эти единицы измерения можно использовать с функцией calc().

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

line-height: calc(1.2 * 1.2);

Её можно использовать и для вычисления углов:

transform: rotate(calc(10deg * 5));

Данную функцию можно применять и в тех случаях, когда в передаваемом ей выражении никаких вычислений не выполняется:

.el {
  /* Выглядит немного странно, но работать будет */
  width: calc(20px);
}

Функцию calc() нельзя применять в медиа-запросах

Хотя эта функция предназначена для установки значений CSS-свойств, она, к сожалению, не работает в медиа-запросах:

@media (max-width: 40rem) {
  /* 40rem или уже */
}

/* Не работает! */
@media (min-width: calc(40rem + 1px)) {
  /* Шире, чем 40rem */
}

Если когда-то подобные конструкции окажутся работоспособными — это будет очень хорошо, так как это позволит создавать взаимоисключающие медиа-запросы, выглядящие весьма логично (например — такие, которые показаны выше).

Использование разных единиц измерения в одном выражении

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

/* Совместное использование процентов и пикселей */
width: calc(100% - 20px);

Это выражение читается так: «Ширина равна ширине элемента, из которой вычитается 20 пикселей».

В ситуации, когда ширина элемента может меняться, совершенно невозможно заранее вычислить нужное значение, пользуясь только показателями, выраженными в пикселях. Другими словами, нельзя провести препроцессинг calc(), используя что-то вроде Sass и пытаясь получить что-то вроде полифилла. Да делать этого и не нужно, учитывая то, что calc() очень хорошо поддерживают браузеры. Смысл тут в том, что подобные вычисления, в которых смешивают значения, выраженные в разных единицах измерения, должны быть произведены в браузере (во время «выполнения» страницы). А именно в возможности выполнения таких вычислений и заключается основная ценность calc().

Вот ещё пара примеров использования значений, выраженных в разных единицах измерения:

transform: rotate(calc(1turn + 45deg));

animation-delay: calc(1s + 15ms);

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

Сравнение calc() с вычислениями, обрабатываемыми препроцессорами

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

$padding: 1rem;

.el[data-padding="extra"] {
  padding: $padding + 2rem; // получается 3rem;
  margin-bottom: $padding * 2; // получается 2rem; 
}

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

Раскрытие смысла используемых в CSS числовых значений

Даже если не пользоваться возможностями, которые достижимы лишь с помощью calc(), эту функцию можно применить для раскрытия смысла применяемых в CSS значений. Предположим, нужно, чтобы значение некоего свойства составляло бы в точности 1/7 ширины элемента:

.el {
  /* Это легче понять, */
  width: calc(100% / 7);

  /* чем это */
  width: 14.2857142857%;
}

Такой подход может оказаться полезным в чём-то вроде некоего самописного CSS-API:

[data-columns="7"] .col { width: calc(100% / 7); }
[data-columns="6"] .col { width: calc(100% / 6); }
[data-columns="5"] .col { width: calc(100% / 5); }
[data-columns="4"] .col { width: calc(100% / 4); }
[data-columns="3"] .col { width: calc(100% / 3); }
[data-columns="2"] .col { width: calc(100% / 2); }

Математические операторы функции calc()

В выражениях, вычисляемых с помощью calc(), можно использовать операторы +-* и /. Но в их применении есть некоторые особенности.

При сложении (+) и вычитании (-) необходимо использовать значения с указанными единицами измерения

.el {
  /* Правильно */
  margin: calc(10px + 10px);

  /* Неправильно */
  margin: calc(10px + 5);
}

Обратите внимание на то, что использование некорректных значений приводит к тому, что конкретное объявление также становится некорректным.

При делении (/) нужно, чтобы у второго числа не была бы указана единица измерения

.el {
  /* Правильно */
  margin: calc(30px / 3);

  /* Неправильно */
  margin: calc(30px / 10px);

  /* Неправильно (на ноль делить нельзя) */
  margin: calc(30px / 0);
}

При умножении (*) у одного из чисел не должна быть указана единица измерения:

.el {
  /* Правильно */
  margin: calc(10px * 3);

  /* Правильно */
  margin: calc(3 * 10px);

  /* Неправильно */
  margin: calc(30px * 3px);
}

О важности пробелов

Пробелы, используемые в выражениях, важны в операциях сложения и вычитания:

.el {
  /* Правильно */
  font-size: calc(3vw + 2px);

  /* Неправильно */
  font-size: calc(3vw+2px);

  /* Правильно */
  font-size: calc(3vw - 2px);

  /* Неправильно */
  font-size: calc(3vw-2px);
}

Тут вполне можно использовать и отрицательные числа (например — в конструкции вроде calc(5vw — -5px)), но это — пример ситуации, в которой пробелы не только необходимы, но ещё и полезны в плане понятности выражений.

Таб Аткинс сказал мне, что причина, по которой операторы сложения и вычитания должны выделяться пробелами, на самом деле, заключается в особенностях парсинга выражений. Не могу сказать, что я в полной мере это понял, но, например, парсер обрабатывает выражение 2px-3px как число 2 с единицей измерения px-3px. А столь странная единица измерения точно никому не понадобится. При парсинге выражений с оператором сложения возникают свои проблемы. Например, парсер может принять этот оператор за часть синтаксической конструкции, используемой при описании чисел. Я подумал было, что пробел нужен для правильной обработки синтаксиса пользовательских свойств (--), но это не так.

При умножении и делении пробелы вокруг операторов не требуются. Но я полагаю, что можно порекомендовать использовать пробелы и с этими операторами — ради повышения читабельности кода, и ради того, чтобы не забывать о пробелах и при вводе выражений, использующих сложение и вычитание.

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

.el {
  /* Допустимо */
  width: calc(
    100%     /   3
  );
}

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

.el {
  /* Неправильно */
  width: calc (100% / 3);
}

Вложенные конструкции: calc(calc())

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

.el {
  width: calc(
    calc(100% / 3)
    -
    calc(1rem * 2)
  );
}

Нет смысла строить вложенные конструкции из функций calc(), так как то же самое можно переписать, используя лишь скобки:

.el {
  width: calc(
   (100% / 3)
    -
   (1rem * 2)
  );
}

Кроме того, в данном примере не нужны и скобки, так как при вычислении представленных здесь выражений применяются правила определения приоритета операторов. Деление и умножение выполняются перед сложением и вычитанием. В результате код можно переписать так:

.el {
  width: calc(100% / 3 - 1rem * 2);
}

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

.el {
  /* То, что получится здесь, */
  width: calc(100% + 2rem / 2);

  /* очень сильно отличается от того, что получится здесь */
  width: calc((100% + 2rem) / 2);
}

Пользовательские CSS-свойства и calc()

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

html {
  --spacing: 10px;
}

.module {
  padding: calc(var(--spacing) * 2);
}

Уверен, несложно представить себе набор CSS-стилей, в котором множество настроек выполняется в одном месте с помощью установки значений пользовательских CSS-свойств. Эти значения потом будут использоваться во всём CSS-коде.

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

html {
  --spacing: 10px;
  --spacing-L: var(--spacing) * 2;
  --spacing-XL: var(--spacing) * 3;
}

.module[data-spacing="XL"] {
  padding: calc(var(--spacing-XL));
}

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

Источником пользовательских свойств может служить HTML-код. Иногда это может оказаться крайне полезным. Например, в Splitting.js к словам и символам так добавляются индексы.

<div style="--index: 1;"> ... </div>
<div style="--index: 2;"> ... </div>
<div style="--index: 3;"> ... </div>
div {
  /* Значение индекса берётся из HTML (с использованием резервного значения) */
  animation-delay: calc(var(--index, 1) * 0.2s);
}

Назначение единиц измерения пользовательским свойствам после объявления этих свойств

Предположим, мы находимся в ситуации, когда в пользовательское свойство имеет смысл записать число без единиц измерения, или в ситуации, когда подобное число удобно будет, перед реальным использованием, как-то преобразовать, не пользуясь единицами измерениями. В подобных случаях пользовательскому свойству можно назначить значение без указания единиц измерения. Когда нужно будет преобразовать это число в новое, выраженное в неких единицах измерения, его можно будет умножить на 1 с указанием нужных единиц измерения:

html {
  --importantNumber: 2;
}

.el {
  /* Число остаётся таким же, каким было, но теперь у него есть единицы измерения */
  padding: calc(var(--importantNumber) * 1rem);
}

Работа с цветами

При описании цветов с использованием форматов, таких, как RGB и HSL, используются числа. С этими числами можно поработать в calc(). Например, можно задать некие базовые HSL-значения, а потом менять их так, как нужно (вот пример):

html {
  --H: 100;
  --S: 100%;
  --L: 50%;
}

.el {
  background: hsl(
    calc(var(--H) + 20),
    calc(var(--S) - 10%),
    calc(var(--L) + 30%)
  )
}

Нельзя комбинировать calc() и attr()

CSS-функция attr() может казаться весьма привлекательной. И правда: берёшь значение атрибута из HTML, а потом его используешь. Но…

<div data-color="red">...</div>
div {
  /* Этого делать нельзя */
  color: attr(data-color);
}

К сожалению, эта функция не понимает «типов» значений. В результате attr() подходит лишь для работы со строками и для установки с её помощью CSS-свойства content. То есть — такая конструкция оказывается вполне рабочей:

div::before {
  content: attr(data-color);
}

Я сказал здесь об этом из-за того, что у кого-нибудь может возникнуть желание попытаться вытащить из HTML-кода с помощью attr() некое число и использовать его в вычислениях:

<div class="grid" data-columns="7" data-gap="2">...</div>
.grid {
  display: grid;

  /* Ни один из этих примеров работать не будет */
  grid-template-columns: repeat(attr(data-columns), 1fr);
  grid-gap: calc(1rem * attr(data-gap));
}

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

<div class="grid" style="--columns: 7; --gap: 2rem;">...</div>
.grid {
  display: grid;

  /* Работает! */
  grid-template-columns: repeat(var(--columns), 1fr);
  grid-gap: calc(var(--gap));
}

Браузерные инструменты

Инструменты разработчика браузера обычно показывают выражения с calc() так, как они описаны в исходном CSS-коде.

Инструменты разработчика Firefox, вкладка Rules
Инструменты разработчика Firefox, вкладка Rules

Если нужно узнать о том, что вычислит функция calc(), можно обратиться к вкладке Computed (такую вкладку можно найти в инструментах разработчиков всех известных мне браузеров). Там и будет показано значение, вычисленное calc().

Инструменты разработчика Chrome, вкладка Computed
Инструменты разработчика Chrome, вкладка Computed

Браузерная поддержка

Здесь можно узнать о поддержке функции calc() браузерами. Если говорить о современных браузерах, то уровень поддержки calc() составляет более 97%. Если же нужно поддерживать довольно старые браузеры (вроде IE 8 или Firefox 3.6), тогда обычно поступают так: добавляют перед свойством, для вычисления значения которого используется calc(), такое же свойство, значение которого задаётся в формате, который понятен старым браузерам:

.el {
  width: 92%; /* Запасной вариант */
  width: calc(100% - 2rem);
}

У функции calc() существует немало известных проблем, но от них страдают лишь старые браузеры. На Caniuse можно найти описание 13 таких проблем. Вот некоторые из них:

  • Браузер Firefox ниже 59 версии не поддерживает calc() в функциях, используемых для задания цвета. Например: color: hsl(calc(60 * 2), 100%, 50%).
  • IE 9-11 не отрендерит тень, заданную свойством box-shadow, в том случае, если для определения любого из значений используется calc().
  • Ни IE 9-11, ни Edge не поддерживают конструкцию вида width: calc() в применении к ячейкам таблиц.

 

Примеры из жизни

Я спросил у нескольких разработчиков, применяющих CSS, о том, когда они в последний раз использовали функцию calc(). Их ответы составили небольшую подборку, которая поможет всем желающим узнать о том, как другие программисты применяют calc() в своей повседневной работе.

  • Я использовал calc() для создания вспомогательного класса для управления полями: .full-bleed { width: 100vw; margin-left: calc(50% — 50vw); }. Могу сказать, что calc() входит в мой топ-3 самых полезных возможностей CSS.
  • Я пользовался этой функцией для выделения пространства под неподвижный «подвал» страницы.
  • Я применял calc() для гибкой настройки шрифтов и параметров текста. А именно, вычислял свойство font-size, основываясь на параметрах области просмотра. Но свойством font-size это не ограничилось — я применял calc() и для вывода значения свойства line-height. (Обратите внимание на то, что если вы используете calc() для динамической настройки шрифтов, и при этом применяете единицы измерения области просмотра, задействуйте единицы измерения, в которых используются rem или em. Это позволит пользователю управлять размерами шрифта, уменьшая или увеличивая масштаб).
  • Мне чрезвычайно нравится возможность применения пользовательского свойства, описывающего ширину содержимого. Им можно пользоваться для настройки расстояния между элементами. Например — так: .margin { width: calc( (100vw — var(--content-width)) / 2); }.
  • Я пользовался calc() для создания кросс-браузерного компонента для вывода текста, в котором первая заглавная буква используется как украшение. Вот часть стиля этого компонента: .drop-cap { --drop-cap-lines: 3; font-size: calc(1em * var(--drop-cap-lines) * var(--body-line-height)); }.
  • Я применял эту функцию для того, чтобы некоторые изображения на странице статьи выходили бы за пределы своих контейнеров.
  • Я использовал её для того чтобы правильно расположить на странице элемент, комбинируя эту функцию со свойством padding и применяя единицы измерения vw/vh.
  • Я применяю calc() для обхода ограничений background-position. В особенности это касается ограничений на настройку позиций изменения цвета в градиентах. Например, это можно описать так: «расположить позицию изменения цвета, не доходя 0.75em до низа».

источник