Как вычисляется значение пользовательских свойств в CSS
Пользовательские свойства — они же переменные CSS
— кажутся довольно простыми. Однако есть особенности поведения, о которых следует знать, когда браузер вычисляет конечные значения. Непонимание этого процесса может привести к неожиданному или отсутствующему значению и сложностям с поиском и устранением проблемы.
Чтобы помочь уверенно использовать пользовательские свойства и эффективно устранять неполадки, мы рассмотрим:
- как браузер определяет значения для любого свойства
- влияние
времени вычисления значения
- подводные камни, связанные с использованием пользовательских свойств в современном CSS
- почему наследование должно лежать в основе архитектуры пользовательских свойств
- стратегии предотвращения некорректных вычисляемых значений
Вычисляемые, наследуемые и начальные значения
Когда браузер анализирует CSS, его цель — вычислить одно значение для свойства для элемента в DOM.
В самом начале изучения CSS вы узнаёте, что значение свойства можно изменять несколько раз с помощью нескольких правил, которые могут выбирать один и тот же элемент.
Если взять HTML <h2 class="card__title">
, то все нижеперечисленные варианты являются подходящими для свойства color
.
body {
color: #222;
}
h2 {
color: #74e;
}
.card__title {
color: #93b;
}
Каждое из этих свойств является объявленным значением, и из-за специфичности и порядка каскада конечное выбранное значение элемента может быть каскадным значением. В данном случае для свойства color
побеждает .card__title
.
Если свойство не получает значения из каскада, то оно будет использовать либо унаследованное, либо начальное значение.
- Наследуемые значения передаются от ближайшего предка, которому присвоено значение, если свойство может наследоваться (например,
color
, свойства шрифта,text-align
). - Начальные значения используются, когда унаследованное значение не существует или недопустимо, и представляют собой значения, предусмотренные спецификацией для свойства
Так, для <h2 class="card__title">
полный набор значений, используемых для этого элемента, может быть следующим:
.card__title {
/* Каскадное значение */
color: #93b;
/* Начальные свойства и значения */
display: block;
/* Наследуемые свойства и значения */
line-height: 1.2;
font-family: Source Code Pro;
font-weight: 500;
font-size: 1.35rem;
}
Некоторые определения свойств требуют дополнительных вычислений для абсолютизации значений. Ниже перечислены некоторые из возможных преобразований значений.
- Относительные единицы, такие как
vw
,em
и%
, преобразуются в пиксельные значения, а плавающие числа могут быть преобразованы в целые. currentColor
и именованные цвета, такие какrebeccapurple
, преобразуются вsRGB
значения.- Композиция значений, влияющих друг на друга.
- например,
padding: 1em
требует вычисления значенияfont-size
, от которого зависитem
.
- например,
- Пользовательские свойства заменяются их вычисленными значениями.
Результатом этих преобразований являются вычисленные, использованные и фактические значения, означающие последовательные шаги, которые могут быть задействованы, чтобы в итоге получить абсолютизированное значение. Вы можете углубиться в специфику вычисления значений или ознакомиться с этим обзором обработки значений.
Пользовательские свойства и время вычисления значения
Одним из особых сценариев вычислений, оказывающих критическое влияние на современный CSS, является присвоение браузером значений пользовательским свойствам, называемое временем вычисления значений
(CVT).
Недействительно на момент времени вычисления значения
Как было описано ранее, обычно незаполненные или недействительные назначения свойств будут возвращаться к каскадным значениям, если это применимо.
/* Используется благодаря каскаду */
p { color: blue }
/* Недопустимо как "цвет", выбрасывается браузером */
.card p { color: #notacolor; }
Попробуйте определить, каким будет значение color
для .card p
в следующем примере.
html { color: red; }
p { color: blue; }
.card { --color: #notacolor; }
.card p { color: var(--color); }
В .card p
будет использовано унаследованное значение color
— red
, предоставленное body
. Он не может использовать каскадное значение blue
из-за того, что браузер отбрасывает его как возможного кандидата на значение во время разбора
, когда он оценивает только синтаксис. Только когда пользовательский агент пытается применить окончательное значение — этап время вычисления значения
— он понимает, что значение недопустимо.
Другими словами: как только браузер определит каскадное значение, которое частично основано на синтаксической корректности, он отбросит все остальные кандидатуры. Для синтаксически корректных пользовательских свойств браузер, по сути, предполагает, что абсолютизированное значение окажется валидным.
Это приводит к тому, что пользовательские свойства не могут отказать раньше времени
. При сбое результирующим значением будет либо унаследованное значение от предка, либо начальное значение свойства. (Если это звучит знакомо, то потому, что такое же поведение наблюдается при использовании unset
).
Критически важно, это означает, что недопустимое значение пользовательского свойства не может вернуться к ранее установленному каскадному значению, как вы ожидаете, потому что они были исключены из дерева решений.
Надежда не потеряна! Если позже утилитарный класс в параграфе обновит свойство color
, то из-за правил каскада и специфичности он выиграет, как обычно, и недействительное значение пользовательского свойства не будет иметь эффекта.
html { color: red; }
p { color: blue; }
.card { --color: #notacolor; }
/* Не используется */
.card p { color: var(--color); }
/* Победитель! */
.card .callout { color: purple }
Обратите внимание, что когда речь идёт о недопустимых значениях для пользовательских свойств, то недопустимым считается то, как это значение применяется. Например, символ пробела является допустимым определением пользовательского свойства, но будет недействительным, если его применить к свойству.
:root {
/* Валидное определение */
--toggle: ;
}
.card {
/* Недействительно во время вычисления */
margin: var(--toggle);
}
С другой стороны, пользовательское свойство со значением 100%
может быть применено к width
, но не к color
.
:root {
--length: 100%;
}
.card {
/* Валидно */
width: var(--length);
/* Недействительно во время вычисления */
color: var(--length);
}
Влияние CVT на поддержку современного CSS
Ещё один сценарий, при котором недействительность пользовательского свойства во время вычисления значения может нарушить ваши ожидания, — это использование пользовательского свойства в качестве частичного значения или неопределённого с запасным значением, особенно в сочетании с современными CSS функциями.
Учитывая вышесказанное, можно ожидать, что когда cqi
не поддерживается, браузер будет просто использовать предыдущее определение font-size
.
h2 {
font-size: clamp(1.25rem, var(--h2-fluid, 1rem + 1.5vw), 2.5rem);
font-size: clamp(1.25rem, var(--h2-fluid, 5cqi), 2.5rem);
}
Вместо этого браузер предполагает, что поймёт второе определение clamp()
, и отбрасывает предыдущие определения font-size
для правила h2
. Но когда браузер переходит к заполнению значения пользовательского свойства и обнаруживает, что не поддерживает cqi
, уже слишком поздно использовать то, что было задумано в качестве запасного определения. Это означает, что вместо него используется исходное значение, если нет наследуемого значения от предка.
Хотя можно подумать, что начальным значением будет, по крайней мере, размер шрифта, соответствующий уровню h2
, начальным значением font-size
любого элемента является "medium", что обычно эквивалентно 1rem
. Это означает, что вы не только теряете задуманный запасной стиль, но и визуальную иерархию h2
в браузерах, не поддерживающих cqi
.
Один из способов узнать initial
значение для любого свойства — найти его на MDN и найти раздел "Формальное определение", в котором будет указано начальное значение, а также может ли это значение наследоваться.
Кроме font-size
, следует обратить внимание на несколько initial
значений:
background-color: transparent
border-color: currentColor
border-width: medium
, что соответствует3px
color: canvastext
, являющийся системным цветом, который, скорее всего, будет черным, но может меняться из-за режимов принудительного выбора цвета.font-family
: зависит от пользовательского агента, скорее всего, это будет шрифт с засечками/serif
Безопасная поддержка современных CSS значений в пользовательских свойствах
Более безопасное решение — обернуть определение, использующее cqi
, в @supports
, чтобы не поддерживающие браузеры действительно использовали запасной вариант.
h2 {
font-size: clamp(1.25rem, var(--h2-fluid, 1rem + 1.5vw), 2.5rem);
}
@supports (font-size: 1cqi) {
h2 {
font-size: clamp(1.25rem, var(--h2-fluid, 5cqi), 2.5rem);
}
}
Означает ли это, что нужно изменить все места, где используются пользовательские свойства? Это зависит от вашей матрицы поддержки (какие браузеры и версии вы решили поддерживать). Для супер-ультрасовременных свойств, особенно если исходное значение нежелательно, такой подход может быть самым безопасным. Другой пример, когда можно использовать условие @supports
, — это новые цветовые пространства, например oklch()
.
Смущает то, что в ситуации, подобной примеру cqi
, браузерные инструменты разработки для не поддерживающего браузера могут всё ещё показывать неработающее правило в качестве применяемого стиля. Вероятно, это связано с тем, что браузер всё ещё поддерживает другие части, например clamp()
. Неправильное отображение в инструментах разработки может затруднить устранение проблем, вызванных тем, что пользовательские свойства недействительны во время вычислений, поэтому важно понимать, что происходит.
Наследование и пользовательские свойства
Ещё один способ влияния времени вычисления значений на назначение значений пользовательских свойств — наследование вычисляемых значений.
Вычисление значения пользовательского свойства выполняется один раз для каждого элемента, после чего вычисленное значение становится доступным для наследования. Давайте узнаем, как это влияет на выбор архитектуры пользовательских свойств.
Наследуемые значения становятся неизменными
Обычно пользовательские свойства объединяются в селектор :root
. Если одно из этих свойств включает вычисление, которое включает другое пользовательское свойство уровня :root
, то обновление изменяющего свойства из потомка не приведёт к обновлению вычисления.
Как показано в следующем примере, значение --font-size-large
вычисляется немедленно, поэтому обновление свойства --font-size
в правиле потомке не сможет повлиять на его значение.
:root {
--font-size: 1rem;
--font-size-large: calc(2 * var(--font-size));
}
h1 {
--font-size: 1.25rem;
/* Новый --font-size не будет обновлять вычисление --font-size-large */
font-size: var(--font-size-large);
}
Это происходит потому, что вычисление происходит сразу после того, как браузер обрабатывает определение :root
. Таким образом, определение :root
создаёт статическое, вычисляемое значение, которое наследуется, но неизменяемо.
Это не значит, что такое поведение характерно только для :root. Ключевая концепция заключается в том, что после вычисления значений пользовательских свойств вычисленное значение только наследуется.
Подумайте об этом по-другому: в рамках каскада значения могут наследоваться потомками, но не могут передавать значения обратно своим предкам. По сути, именно поэтому вычисленное значение пользовательского свойства элемента предка не может быть изменено элементом потомком.
Включение расширяемых значений пользовательских свойств
Если понизим вычисление пользовательского свойства, чтобы оно применялось на основе классов, то браузер сможет пересчитывать в процессе обработки значения для определения вычисленного значения. Это связано с тем, что он будет вычислять значение для элементов с классом font-resize
и отдельное значение для элементов с классами font-resize
и font-large
.
:root {
--font-size: 1rem;
}
.font-resize {
font-size: calc(var(--font-size-adjust, 1) * var(--font-size));
}
.font-large {
/* Успешно изменяет значение в паре с .font-resize */
--font-size-adjust: 2.5;
}
Предотвращение не валидности во время вычисления значения
Несколько простых стратегий, позволяющих избежать неудач с пользовательскими свойствами, включают в себя:
- Используйте запасное значение при определении пользовательского свойства, являющееся вторым значением, которое может быть передано в функцию
var()
, напримерvar(--my-property, 1px)
. - Убедитесь, что запасные значения в
var()
имеют правильный тип свойства, или определите собственный тип с помощью@property
. - Если используете полифилл для новой функции, проверьте, что он разрешает использование в пользовательских свойствах так, как ожидается.
- Используйте
@supports
, чтобы убедиться, что запланированное обновление до современного CSS не нарушит запасные правила в неподдерживающих браузерах.
И, как всегда — тестируйте свои решения во всех браузерах и на всех устройствах которые вам доступны!
Предоставьте пользовательское начальное значение с @property
Кроссбраузерной особенностью с момента выхода Firefox 128 в июле 2024 года станет новое at-правило — @property
, позволяющее определять типы для пользовательских свойств.
Параметр initial-value
позволяет определить собственное начальное значение свойства, которое будет использоваться в случае, если вычисленное значение окажется недействительным!
Учитывая следующее определение пользовательского свойства --color-primary
, если вычисленное значение окажется недействительным, вместо него будет использовано предоставленное начальное значение purple
.
@property --color-primary {
syntax: "<color>";
inherits: true;
initial-value: purple;
}
Более подробно о том, как использовать @property
, можно узнать из статьи о Современном CSS Providing Type Definitions for CSS with @property
.