Предоставление определения типа для CSS с @property

Источник: «Providing Type Definitions for CSS with @property»
Пишите более безопасный CSS, используя @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 и посмотрим, даёт ли он подсказку, что происходит.

Панель стилей в DevTools показывает .color-blue и правило параграфа, без видимой ошибки

Панель стилей в DevTools показывает .color-blue и правило параграфа, без видимой ошибки

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

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

: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 на самом деле не является цветом а, следовательно, недействителен, лучшее, что он может сделать, это использовать любой из них:

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

Определение типов для безопасного CSS

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

Важнейшими возможностями @property являются:

Это at-правило определяется для каждого объекта, то есть для каждого объекта, для которого вы хотите использовать эти преимущества, требуется уникальное определение.

Это не является обязательным, и можно продолжать использовать пользовательские свойства без применения @property.

Давайте применим его к синей дилемме и посмотрим, как он устраняет проблему недопустимого цвета, указанного в правиле элемента.

@property --color-blue {
syntax: "<color>";
inherits: true;
initial-value: blue;
}

/* Предыдущие правила также применяются */

Успешно, текст по-прежнему синий, несмотря на неверное определение!

Кроме того, DevTools снова стал полезным:

DevTools отображает недопустимое значение в правиле параграфа как перечёркнутое и со значком ошибки, а также при наведении на пользовательское свойство --color-blue предоставляет подсказку с полным определением пользовательского свойства в @property

DevTools отображает недопустимое значение в правиле параграфа как перечёркнутое и со значком ошибки, а также при наведении на пользовательское свойство --color-blue предоставляет подсказку с полным определением пользовательского свойства в @property

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

Предоставление типов через syntax

Зачем нужны типы для пользовательских свойств? Вот несколько причин:

В определении @property дескриптор syntax позволяет указать допустимые типы для пользовательского свойства. В данном случае используется "<color>", но возможны и другие типы:

Особым случаем является "*", обозначающий "универсальный синтаксис" и позволяющий принимать любое значение, аналогично поведению по умолчанию. Это означает, что не нужно набирать текст, но, возможно, необходимо наследование и/или контроль начального значения.

Эти и другие типы перечислены в дескрипторе 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>";

Типизация для нескольких значений

До сих пор мы вводили только те пользовательские свойства, которые ожидают одно значение.

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

Это может быть удобно для свойств, допускающих несколько определений, таких как 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 — имеет последствия и ограничивает использование свойства.

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

Один из приёмов, который я люблю использовать для создания гибких стилей компонентов, — это включение заведомо неопределённого пользовательского свойства, чтобы можно было эффективно создавать варианты, просто изменяя значение пользовательского свойства. Или же намеренное использование полностью неопределённых свойств для того, чтобы сделать базовый класс более подходящим для различных сценариев, рассматривая пользовательские свойства как 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 позволяет анимировать диапазон цветов для фона.

See the Pen



Использование @property позволяет писать более безопасные пользовательские свойства CSS, что повышает надёжность конструкции системы, а также защищает от ошибок, способных повлиять на пользовательский опыт. Напоминаем, что на данный момент они являются прогрессивным улучшением и почти всегда должны использоваться в сочетании с явным определением свойства.

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

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

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

MySQL 9.0 Community Edition: Ключевые возможности и улучшения

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

Разделение хостов баз данных для оптимизации в Laravel