Как вычисляется значение пользовательских свойств в CSS

Источник: «How Custom Property Values are Computed»
Ознакомьтесь с поведением, о котором следует знать, как браузер вычисляет окончательные значения пользовательских свойств. Непонимание этого процесса может привести к неожиданному или отсутствующему значению, а также к трудностям с поиском и устранением проблемы.

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

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

Вычисляемые, наследуемые и начальные значения

Когда браузер анализирует CSS, его цель — вычислить одно значение для свойства для элемента в DOM.

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

Если взять HTML <h2 class="card__title">, то все нижеперечисленные варианты являются подходящими для свойства color.

body {
color: #222;
}

h2 {
color: #74e;
}

.card__title {
color: #93b;
}

Каждое из этих свойств является объявленным значением, и из-за специфичности и порядка каскада конечное выбранное значение элемента может быть каскадным значением. В данном случае для свойства color побеждает .card__title.

Если свойство не получает значения из каскада, то оно будет использовать либо унаследованное, либо начальное значение.

Так, для <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;
}

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

  1. Относительные единицы, такие как vw, em и %, преобразуются в пиксельные значения, а плавающие числа могут быть преобразованы в целые.
  2. currentColor и именованные цвета, такие как rebeccapurple, преобразуются в sRGB значения.
  3. Композиция значений, влияющих друг на друга.
    1. например, padding: 1em требует вычисления значения font-size, от которого зависит em.
  4. Пользовательские свойства заменяются их вычисленными значениями.

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

Пользовательские свойства и время вычисления значения

Одним из особых сценариев вычислений, оказывающих критическое влияние на современный 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 будет использовано унаследованное значение colorred, предоставленное 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 значений:

Безопасная поддержка современных 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;
}

Предотвращение не валидности во время вычисления значения

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

И, как всегда — тестируйте свои решения во всех браузерах и на всех устройствах которые вам доступны!

Предоставьте пользовательское начальное значение с @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.

Дополнительные материалы

Предыдущая Статья

Отпечатки запросов и как их использовать в Laravel

Следующая Статья

Прекратите использовать MD5 и SHA-1!