Предоставление определения типа для CSS с @property
@property
, позволяющий определять типы для пользовательских свойств. Узнайте, почему традиционные запасные значения могут не сработать и как функции @property
повышают устойчивость определений пользовательских свойств.Кроссбраузерной возможностью с момента выхода Firefox 128 в июле 2024 года стало новое at-правило @property
, позволяющее определять типы, а также наследование и начальное значение для пользовательских свойств.
Разберёмся, когда и почему традиционные запасные значения могут не сработать, и как функции @property
позволяют писать более безопасные и устойчивые определения пользовательских CSS свойств.
Стандартное использование пользовательских свойств
Пользовательские свойства — они же "CSS переменные" — полезны тем, что позволяют создавать ссылки на значения, аналогичные переменным в других языках программирования.
Рассмотрим следующий сценарий, в котором создаётся и присваивается свойство --color-blue
, затем реализуемое как класс и применяемое к параграфу.
:root {
--color-blue: blue;
}
.color-blue {
color: var(--color-blue);
}
Параграф отображается синим цветом. Отлично! Отправляйте.
Распространённые ошибки при использовании пользовательских свойств
Мы с вами знаем, что "blue"/"синий" — это цвет. Также может показаться очевидным, что будет применяться класс, использующий этот цвет, только к тексту, который мы решили сделать синим.
Но реальный мир не идеален, и иногда у последующих пользователей CSS появляется причина переопределить значение. Или, возможно, они случайно внесут опечатку, влияющую на исходное значение.
Результатом любого из этих сценариев может стать:
- текст приобретает не синий цвет, как предполагал автор
- текст неожиданно отображается как чёрный
Если отрисованный цвет на удивление чёрный, то, скорее всего, мы столкнулись с уникальным сценарием недействительным на момент вычисления значением.
Когда браузер оценивает CSS правила и решает, какое значение применить к каждому свойству, основываясь на каскаде, наследовании, специфичности и так далее, он сохранит пользовательское свойство как выигрышное значение до тех пор, пока понимает общий способ его использования.
В нашем примере --color-blue
браузер определённо понимает CSS свойство color
, поэтому он предполагает, что и с использованием переменной всё будет в порядке.
Но что произойдёт, если кто-то переопределит --color-blue
в недопустимый цвет?
:root {
--color-blue: blue;
}
.color-blue {
color: var(--color-blue);
}
p {
--color-blue: notacolor;
}
Удивительно, но он отображается как чёрный.
Почему традиционные запасные значения могут не работать
Итак, прежде чем узнать, что означает эта пугающая фраза, давайте заглянем в DevTools и посмотрим, даёт ли он подсказку, что происходит.
Всё выглядит вполне нормально и не показывает, что что-то не так, что значительно усложняет поиск ошибки.
Возможно, вы знаете, что пользовательские свойства позволяют указывать запасное значение в качестве второго параметра, так что, возможно, это поможет! Давайте попробуем.
:root {
--color-blue: blue;
}
.color-blue {
color: var(--color-blue, blue);
}
p {
--color-blue: notacolor;
}
К сожалению, текст по-прежнему отображается черным.
Хорошо, но добрый друг каскад существует, и в своё время мы помещали такие вещи, как префиксные свойства вендора, перед непрефиксными. Так что, возможно, если использовать аналогичный метод и поставить дополнительное определение color
перед определением, имеющим пользовательское свойство, оно сможет вернуться к нему?
:root {
--color-blue: blue;
}
.color-blue {
color: blue;
color: var(--color-blue);
}
p {
--color-blue: notacolor;
}
Облом, похоже, прогресс в предотвращении этой проблемы не наблюдается.
Это связано с (немного пугающим по звучанию) сценарием, недействительным на момент вычисления значением.
В этом случае он рассматривает и класс .color-blue
, и значение, предоставленное правилом элемента p
, и пытается применить вычисленное значение notacolor
. На этом этапе он отбросил альтернативное значение blue
, изначально предоставленное классом. Следовательно, поскольку notacolor
на самом деле не является цветом а, следовательно, недействителен, лучшее, что он может сделать, это использовать любой из них:
- унаследованное значение, если свойство может наследоваться и предок предоставил значение; или
- начальное значение, как определено в спецификации CSS
Хотя color
является наследуемым свойством, оно не было определено ни для одного из предков, поэтому отрисованный цвет black
обусловлен начальным значением.
Определение типов для безопасного CSS
Пришло время представить @property
, помогающую решить проблему того, что вы можете воспринять как неожиданное отображаемое значение.
Важнейшими возможностями @property
являются:
- определение допустимых типов для конкретных пользовательских свойств
- включение или отключение наследования
- предоставление начального значения в качестве защиты от недействительных или неопределённых значений
Это at-правило определяется для каждого объекта, то есть для каждого объекта, для которого вы хотите использовать эти преимущества, требуется уникальное определение.
Это не является обязательным, и можно продолжать использовать пользовательские свойства без применения @property
.
Давайте применим его к синей дилемме и посмотрим, как он устраняет проблему недопустимого цвета, указанного в правиле элемента.
@property --color-blue {
syntax: "<color>";
inherits: true;
initial-value: blue;
}
/* Предыдущие правила также применяются */
Успешно, текст по-прежнему синий, несмотря на неверное определение!
Кроме того, DevTools снова стал полезным:
Можно заметить, что недопустимое значение явно является ошибкой, а также предоставляется полное определение пользовательского свойства с помощью всплывающей подсказки.
Предоставление типов через syntax
Зачем нужны типы для пользовательских свойств? Вот несколько причин:
- типы помогают проверить, что является допустимым, а что недопустимым значением.
- без типов пользовательские свойства неограниченны и могут принимать практически любое значение, включая пробел
- отсутствие типов не позволяет DevTools браузера предоставить оптимальный уровень детализации, какое значение используется для пользовательского свойства
В определении @property
дескриптор syntax
позволяет указать допустимые типы для пользовательского свойства. В данном случае используется "<color>"
, но возможны и другие типы:
"<length>"
— числа с единицами измерения, например,4px
или3vw
"<integer>"
— десятичные единицы от 0 до 9 (они же "целые числа")"<number>"
— числа, которые могут содержать дробную часть, например1.25
"<percentage>"
— числа со знаком процента, например24 %
."<length-percentage>"
— принимает допустимые значения<length>
или<percentage>
.
Особым случаем является "*"
, обозначающий "универсальный синтаксис" и позволяющий принимать любое значение, аналогично поведению по умолчанию. Это означает, что не нужно набирать текст, но, возможно, необходимо наследование и/или контроль начального значения.
Эти и другие типы перечислены в дескрипторе syntax
на MDN.
Тип применяется к вычисляемому значению пользовательского свойства, поэтому для типа "<color>"
подойдёт как blue
, так и light-dark(blue, cyan)
(хотя в initial-value
, как вскоре увидим, принимается только один из них).
Строгая типизация со списками
Допустим, необходимо обеспечить небольшую гибкость для пользовательского свойства --color-blue
.
Можно использовать список для указания допустимых вариантов. Любые другие значения, кроме этих, будут считаться недействительными, и вместо них будет использоваться значение initial-value
(если не применяется наследование). Они называются "пользовательскими идентификаторами", чувствительны к регистру и могут иметь любое значение.
@property --color-blue {
syntax: "blue | cyan | dodgerblue";
inherits: true;
initial-value: blue;
}
.color-blue {
color: var(--color-blue);
}
.demo p {
--color-blue: dodgerblue;
}
Типизация для смешанных значений
Символ pipe (|
), использованный в предыдущем списке, обозначает условие "или". Хотя использовались явные названия цветов, его также можно использовать, чтобы сказать: Любой из этих типов синтаксиса допустим
.
syntax: "<color> | <length>";
Типизация для нескольких значений
До сих пор мы вводили только те пользовательские свойства, которые ожидают одно значение.
Два дополнительных варианта могут быть охвачены дополнительным символом "множитель", который должен следовать сразу за синтаксическим именем компонента.
- Используйте
+
для поддержки списка, разделённого пробелами, например,"<length>+"
. - Используйте
#
для поддержки списка, разделённого запятыми, например,"<length>#"
.
Это может быть удобно для свойств, допускающих несколько определений, таких как background-image
.
@property --bg-gradient{
syntax: "<image>#";
inherits: false;
initial-value:
repeating-linear-gradient(to right, blue 10px 12px, transparent 12px 22px),
repeating-linear-gradient(to bottom, blue 10px 12px, transparent 12px 22px);
}
.box {
background-image: var(--bg-gradient);
inline-size: 5rem;
aspect-ratio: 1;
border-radius: 4px;
border: 1px solid;
}
Типизация многокомпонентных смешанных значений
Некоторые свойства принимают смешанные типы для получения полного значения, например, box-shadow
имеет потенциальные типы inset
, ряд значений <length>
и <color>
.
В настоящее время невозможно типизировать это в одном определении @property
, хотя можно попытаться сделать что-то вроде "<length>+ <color>"
. Однако это фактически делает недействительным само определение @property
.
Одна из альтернатив — разбить определения пользовательских свойств так, чтобы разрешить серию длин, а затем разрешить цвет. Хотя это и несколько более громоздко, но позволяет получить преимущество типизации, что избавляет от возможных ошибок, о которых говорилось ранее.
@property --box-shadow-length {
syntax: "<length>+";
inherits: false;
initial-value: 0px 0px 8px 2px;
}
@property --box-shadow-color {
syntax: "<color>";
inherits: false;
initial-value: hsl(0 0% 75%);
}
.box {
box-shadow: var(--box-shadow-length) var(--box-shadow-color);
inline-size: 5rem;
aspect-ratio: 1;
border-radius: 4px;
}
Допустимость любого типа
Если "тип" свойства, например box-shadow
, волнует меньше, а наследование или начальное значение — больше, можно воспользоваться универсальным определением синтаксиса и разрешить любое значение. Это сводит на нет проблему, которую только что удалось решить с помощью разделения частей.
@property --box-shadow {
syntax: "*";
inherits: false;
initial-value: 0px 0px 8px 2px hsl(0 0% 75%);
}
Поскольку универсальный синтаксис принимает любое значение, дополнительный "множитель" не нужен.
Изменение наследования
Некоторые свойства CSS наследуются, например, color
. Дескриптор inherits
для регистрации @property
позволяет контролировать это поведение для пользовательского свойства.
Если значение true
, то вычисляемое значение может искать своё значение у предка, если свойство не задано явно, а если значение не найдено, то будет использовано начальное значение.
Если false
, то вычисляемое значение будет использовать начальное значение, если свойство не задано явно для элемента, например, через правило класса или элемента.
В этой демонстрации для параметра --box-bg
было установлено значение inherits: false
, и только внешний бокс имеет явное определение через прикладной класс. Внутренний бокс использует начальное значение, поскольку наследование не разрешено.
@property --box-bg {
syntax: "<color>";
inherits: false;
initial-value: cyan;
}
.box {
background-color: var(--box-bg);
aspect-ratio: 1;
border-radius: 4px;
padding: 1.5rem;
}
.outer-box {
--box-bg: purple;
}
Валидное использование initial-value
Если синтаксис не является открытым для любого значения, используя универсальное определение синтаксиса — "*"
, — то для получения преимуществ регистрации пользовательского свойства необходимо задать initial-value
.
Как уже было сказано, использование initial-value
было критически важно для предотвращения ситуации, когда рендер был полностью сломан из-за недействительного на момент вычисления значения. Вот некоторые другие преимущества использования @property
с initial-value
.
При создании систем дизайна или библиотек пользовательского интерфейса важно обеспечить надёжность и стабильность пользовательских свойств. Предоставление initial-value
может помочь предотвратить сбои в работе. Кроме того, типизация свойств также хорошо сочетается с сохранением смысла токенов дизайна, которые могут быть выражены как пользовательские свойства.
Сценарии динамических вычислений, такие как использование clamp()
, потенциально могут включать недопустимое значение, как из-за ошибки, так и из-за того, что браузер не поддерживает что-то в функции. Наличие запасного значения через initial-value
гарантирует, что дизайн останется функциональным. Такое возвратное поведение также является защитой от неподдерживаемых функций, хотя это может быть ограничено тем, поддерживается ли правило @property
в используемом браузере.
Использование @property
с initial-value
не только повышает надёжность CSS, но и открывает возможности для более эффективного использования пользовательских свойств. Мы предварительно проверили изменение поведения в DevTools браузера, но я надеюсь на расширение инструментария, включая IDE плагины.
Дополнительный уровень безопасности от использования @property
с initial-value
помогает сохранить замысел вашего дизайна, даже если он не является идеальным для каждого контекста.
Ограничения initial-value
Значение initial-value
зависит от синтаксиса, определённого для @property
. Кроме того, сам синтаксис не поддерживает все возможные комбинации значений, о чем говорилось ранее. Поэтому иногда нужно проявить творческий подход, чтобы извлечь пользу.
Кроме того, значения initial-value
должно быть тем, что в спецификации называется независимым от вычислений. В упрощённом виде это означает, что относительные значения типа em
или динамические функции типа clamp()
или light-dark()
, к сожалению, недопустимы. Однако в этих сценариях всё же можно задать приемлемое начальное значение, а затем использовать относительное или динамическое значение при использовании пользовательского свойства, как, например, в назначении :root
.
@property --heading-font-size {
syntax: "<length>";
inherits: true;
initial-value: 24px;
}
:root {
--heading-font-size: clamp(1.25rem, 5cqi, 2rem);
}
Это ограничение на относительные единицы или динамические функции также означает, что другие пользовательские свойства не могут быть использованы в присвоении initial-value
. Для смягчения этой проблемы можно использовать предыдущую технику, когда предпочтительный результат состоит в использовании свойства.
Наконец, пользовательские свойства, зарегистрированные с помощью @property
, по-прежнему ограничены правилами обычных свойств, например, их нельзя использовать для включения переменных в at-правила медиа или контейнерных запросов. Например, @media (min-width: var(--mq-md))
будет по-прежнему недопустимым.
Неподдерживаемое значение initial-value
может привести к сбою страницы
На момент написания статьи () использование свойства или значения функции, не поддерживаемого браузером, в качестве части определения initial-value
может привести к сбою всей страницы!
К счастью, с помощью @supports
можно проверить наличие ультрасовременных свойств или возможностей, прежде чем пытаться использовать их в качестве initial-value
.
@supports ([property|feature]) {
/* Функция поддерживается, используйте для initial-value */
}
@supports not ([property|feature]) {
/* Функция не поддерживается, используйте альтернативный вариант для initial-value */
}
Ещё могут быть сюрпризы, когда @supports
возвращает истину, но тестирование выявляет сбой или другую ошибку (например, currentColor
, используемый с color-mix()
в Safari). Обязательно тестируйте ваши решения кроссбраузерно!
Подробнее о способах проверки поддержки современных CSS.
Исключения динамических ограничений
Есть несколько условий, которые могут показаться исключением из требования "независимых от вычислений" значений, когда они используются для initial-value
.
Во-первых, принимается значение currentColor
. В отличие от относительного значения, такого как em
, требующего вычисления font-size
предков, значение currentColor
может быть вычислено без зависимости от контекста.
@property --border-color {
syntax: "<color>";
inherits: false;
initial-value: currentColor;
}
h2 {
color: blue;
border: 3px solid var(--border-color);
}
Во-вторых, использование "<length-percentage>"
позволяет использовать calc()
, о чем говорится в спецификации. Это позволяет вычислять то, что считается глобальным, независимым от вычислений набором единиц, хотя часто используется для динамического поведения. То есть, использование единиц области просмотра.
Для сценария типа fluid это лучший вариант, сохраняющий дух задуманного результата, хотя в целом он менее идеален для большинства сценариев.
@property --heading-font-size {
syntax: "<length-percentage>";
inherits: true;
initial-value: calc(18px + 1.5vi);
}
/* На практике, определите свою идеальную функцию изменения
размера используя `clamp()` через присваивание `:root`. */
h2 {
font-size: var(--heading-font-size);
}
Последствия установки initial-value
В некоторых сценариях регистрация свойства без универсального синтаксиса — а это означает, что требуется initial-value
— имеет последствия и ограничивает использование свойства.
Среди причин, по которым можно отдать предпочтение необязательным свойствам компонентов, можно назвать следующие:
- использование обычного метода резервного значения пользовательского свойства для значения по умолчанию, особенно если резервное значение должно быть другим пользовательским свойством (например, токеном дизайна)
initial-value
может привести к нежелательному состоянию по умолчанию, тем более что оно не может включать другое пользовательское свойство
Один из приёмов, который я люблю использовать для создания гибких стилей компонентов, — это включение заведомо неопределённого пользовательского свойства, чтобы можно было эффективно создавать варианты, просто изменяя значение пользовательского свойства. Или же намеренное использование полностью неопределённых свойств для того, чтобы сделать базовый класс более подходящим для различных сценариев, рассматривая пользовательские свойства как API для стилей компонентов.
Например, если зарегистрировать --button-background
как цвет, он никогда не будет использовать правильное резервное значение, когда я хочу, чтобы резервное значение использовалось в варианте по умолчанию.
.button {
/* Использование initial-value не позволит использовать резервное значение */
background-color: var(--button-background, var(--color-primary));
/* Задуман как неопределённый и поэтому считается недопустимым, пока не установлен */
border-radius: var(--button-border-radius);
}
.button--secondary {
--button-background: var(--color-secondary);
}
.button--rounded {
--button-border-radius: 4px;
}
Если столкнётесь с подобными сценариями, можете использовать смешанный подход, когда типизируются примитивные свойства — например, --color-primary
— но не специфические свойства компонентов.
Советы по использованию @property
Хотя некоторые примеры в статье были специально настроены так, чтобы при отображении использовалось только значение initial-value
, на практике лучше всего отдельно определить пользовательское свойство. Опять, в данный момент это новая функция, поэтому без дополнительного определения, например, в :root
, есть риск вообще остаться без значения, если перейти на использование только initial-value
.
Кроме того, необходимо помнить, что одно и то же свойство можно зарегистрировать несколько раз, а каскадные правила означают, что победит последнее. Это повышает вероятность конфликтов из-за случайных переопределений. Не существует способа "ограничить" правило @property
внутри селектора.
Однако использование каскадных слоёв может изменить это поведение, поскольку неслоистые стили выигрывают у слоистых, что включает и at-правила. Каскадные слои могут быть способом управления регистрацией правил @property
, если назначить слой "properties" на ранней стадии и взять на себя обязательство назначать все регистрации в этом слое.
Пользовательские свойства также могут быть зарегистрированы через JavaScript. На самом деле, это был первоначальный способ сделать это, так как данная возможность изначально была связана с Houdini API. Если свойство зарегистрировано через JS, то это определение, скорее всего, выиграет у того, которое находится в таблице стилей. Тем не менее, если действительно хотите изменить значение пользовательского свойства с помощью JS, изучите более подходящий способ доступа и установки пользовательских свойств с помощью JS.
Использование @property
может усилить запросы контейнерного стиля, особенно если регистрировать свойства для работы в качестве переключателей или перечислений. В этом примере использование @property
помогает типизировать значения темы и обеспечивает резервное значение "light".
@property --theme {
syntax: "light | dark";
inherits: true;
initial-value: light;
}
:root {
--theme: dark;
}
@container style(--theme: dark) {
body {
background-color: black;
color: white;
}
}
Хотя это немного выходит за рамки данной статьи, ещё одним преимуществом типизации пользовательских свойств является то, что они становятся анимируемыми. Это происходит потому, что тип превращает значение в нечто, с чем CSS знает, как работать, а не в загадочное неопределённое значение, каким оно было бы в противном случае. Вот пример из CodePen, как регистрация пользовательского свойства color
позволяет анимировать диапазон цветов для фона.
Использование @property
позволяет писать более безопасные пользовательские свойства CSS, что повышает надёжность конструкции системы, а также защищает от ошибок, способных повлиять на пользовательский опыт. Напоминаем, что на данный момент они являются прогрессивным улучшением и почти всегда должны использоваться в сочетании с явным определением свойства.
Обязательно протестируйте, чтобы убедиться в правильности выбранного синтаксиса и результата, если initial-value
будет использовано в финальном рендере.