Back

Работа с типизированными CSS-переменными с помощью @property

Работа с типизированными CSS-переменными с помощью @property

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

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

Ключевые выводы

  • Нетипизированное пользовательское CSS-свойство является строкой; @property присваивает ему тип, который браузер проверяет при каждом присвоении.
  • Дескрипторы syntax и inherits всегда обязательны, а initial-value обязателен всякий раз, когда syntax не равен "*" — согласно спецификации CSS Properties and Values API Level 1.
  • Когда зарегистрированное свойство получает значение, не соответствующее объявленному syntax, браузер молча возвращается к initial-value без ошибки в консоли и без визуального индикатора срабатывания отката.
  • Типизированные свойства интерполируются во время переходов и анимаций; нетипизированные пользовательские свойства — нет, поскольку браузер воспринимает их как две непрозрачные строки без числовой средней точки.
  • @property входит в Baseline Newly Available начиная с 9 июля 2024 года — оговорки об «экспериментальности» в материалах до 2024 года устарели.

Проблема: нетипизированные пользовательские свойства — это просто строки

Стандартное пользовательское CSS-свойство хранит неразобранную строку вплоть до её подстановки в реальное свойство. Браузер не знает, является ли --accent цветом, длиной или ключевым словом. Он не выполняет валидацию в месте объявления, не может интерполировать между двумя значениями во время анимации и не сообщает вам, когда значение структурно неверно для предполагаемого использования.

Третий пробел наиболее ощутим на практике. Рассмотрим нетипизированное свойство, используемое в text-shadow:

.card {
  --accent: red;
  text-shadow: 4px 2px 5px var(--accent);
}

/* где-то ещё, по ошибке */
.card {
  --accent: 20px;
}

Объявление text-shadow становится недействительным в момент подстановки, и тень исчезает. Никакого предупреждения в месте, где --accent было установлено в 20px, нет — в этот момент это по-прежнему просто строка. У браузера нет представления о том, что данное свойство должно быть цветом. Руководство MDN по пользовательским свойствам описывает эту модель подстановки: значение пользовательского свойства разрешается только при его использовании через var().

@property из спецификации CSS Properties and Values API Level 1 добавляет тип к самому свойству. После регистрации браузер знает, что --accent является <color>, и применяет это при каждом присвоении, а не только при подстановке.

Синтаксис: три дескриптора и их обязательность

At-правило @property принимает три дескриптора: syntax, inherits и initial-value. Дескрипторы syntax и inherits всегда обязательны; initial-value обязателен всякий раз, когда syntax не равен "*" — его отсутствие в типизированной регистрации делает весь блок @property недействительным и игнорируемым.

@property --accent {
  syntax: "<color>";
  inherits: false;
  initial-value: #586de7;
}
  • syntax — строка, описывающая принимаемый тип, взятая из фиксированного набора поддерживаемых имён, определённых спецификацией (рассматривается в следующем разделе).
  • inherits — булево значение (true или false), определяющее, наследуется ли свойство по дереву DOM. Это то же поведение наследования, что и у любого CSS-свойства; явное указание делает типизированные свойства предсказуемыми во вложенных компонентах.
  • initial-value — значение, используемое, когда никакое другое допустимое значение не применяется, а также значение, к которому свойство откатывается при некорректных входных данных.

Спецификация CSS Properties and Values API Level 1, §3.1 точно определяет требования: правило @property недействительно при отсутствии syntax или inherits, а также недействительно при отсутствии initial-value, если только syntax не является универсальным "*". В ряде существующих руководств initial-value описывается как безусловно обязательный; спецификация же связывает это требование со значением syntax, а inherits не влияет на данное условие. Недействительное правило @property отбрасывается — регистрация попросту не происходит, и свойство возвращается к нетипизированному поведению.

Реестр типов CSS @property

Дескриптор syntax принимает поддерживаемые имена синтаксических компонентов, определённые в спецификации CSS Properties and Values API Level 1, §2 — включая <color>, <length>, <percentage>, <integer>, <angle>, <image> и <custom-ident> — а также мультипликаторы (+ для списков с пробелами, # для списков с запятыми) и синтаксис объединения (<color> | <length>) для свойств, правомерно принимающих несколько типов. Это фиксированный список поддерживаемых имён, а не открытый доступ к любому ключевому слову CSS-типа.

Значение syntaxПринимаетОтклоняетПример initial-value
"<color>"любой допустимый цвет (#f00, rebeccapurple, oklch(...))длины, ключевые слова вроде darkpink#586de7
"<length>"px, rem, em, vw и т.д.числа без единиц, проценты20px
"<percentage>"50%длины, числа без единиц100%
"<integer>"целые числа (12)1.5, длины12
"<angle>"deg, rad, turn, gradчисла без единиц0deg
"<image>"url(...), градиентыцвета, длиныurl(bg.png)
"<custom-ident>"авторские идентификаторычисла, строки в кавычкахnone
"*"любое значение (нетипизированный проброс)ничего — принимает всёнеобязателен

Три грамматических расширения расширяют диапазон значений, принимаемых одним свойством:

/* "+" — список длин, разделённых пробелами */
@property --insets {
  syntax: "<length>+";
  inherits: false;
  initial-value: 0px;
}

/* "#" — список цветов, разделённых запятыми */
@property --stops {
  syntax: "<color>#";
  inherits: false;
  initial-value: black;
}

/* "|" — объединение: принимает либо длину, либо ключевое слово "auto" */
@property --gap {
  syntax: "<length> | auto";
  inherits: false;
  initial-value: auto;
}

Мультипликатор + означает список, разделённый пробелами; # — список, разделённый запятыми, согласно разделу о поддерживаемых строках синтаксиса спецификации. Объединение | позволяет свойству принимать более одного типа — полезно для свойств, которые действительно принимают, например, длину или ключевое слово. Универсальный синтаксис "*" полностью отключает проверку типов; это единственный случай, когда initial-value необязателен, поскольку нет типа, к которому нужно возвращаться по умолчанию. Для определений по типам справочник по типам значений CSS на MDN и индекс типов CSSWG перечисляют каждое имя компонента.

Валидация: тихий откат, которого вы не ожидаете

Когда зарегистрированное свойство получает значение, не соответствующее объявленному syntax, браузер отбрасывает присвоение и отображает элемент, используя initial-value. В современных браузерах этот откат не порождает ни ошибки в консоли, ни визуального индикатора на отображаемой странице — страница не ломается, но и не сообщает вам о том, что что-то пошло не так.

@property --hue {
  syntax: "<angle>";
  inherits: false;
  initial-value: 90deg;
}

.card {
  --hue: 220deg;            /* ✅ допустимо, используется */
  --hue: #f00;              /* ❌ недопустимый тип, игнорируется — откат к 90deg */
  background: oklch(70% 0.15 var(--hue));
}

background всегда разрешается в допустимый цвет. После недопустимого присвоения --hue не становится #f00 и не становится пустым — недопустимое значение отбрасывается, и свойство разрешается в зарегистрированный initial-value равный 90deg. Страница MDN по @property документирует это как то, что свойство становится «недействительным во время вычисления значения», что разрешается в зарегистрированное начальное значение.

Это действительно лучше, чем явно сломанный макет. Но это также новый класс сбоев. Нетипизированные пользовательские свойства ломаются явно — зависимое объявление нарушается, и вы это видите. Типизированные свойства ломаются тихо: JS-переключатель тем записывает некорректный цвет, пользовательское значение не разбирается, дизайн-токен получает неверную единицу измерения — и компонент отображается в своём состоянии по умолчанию без каких-либо следов ошибки. DevTools показывает вычисленное резервное значение при инспектировании элемента, но в консоли во время выполнения ничего не появляется.

Именно такой класс ошибок призван выявлять session replay. Когда типизированное пользовательское свойство получает некорректные входные данные в продакшене — от пользовательского ввода, неправильно настроенного токена или изменения темы во время выполнения — браузер молча откатывается к initial-value, ошибка JavaScript не возникает, и стандартный мониторинг ошибок не подаёт никакого сигнала. Единственное свидетельство после развёртывания — визуальное: компонент отображается в неверном цвете или размере. Записи сессий таких реализаций нередко напрямую фиксируют некорректное состояние, захватывая отображаемый DOM в момент присвоения неверного значения там, где инструмент, работающий только с консолью, ничего не видит.

Анимация: интерполяция типизированных свойств

Зарегистрированные пользовательские свойства интерполируются во время анимаций; незарегистрированные вместо этого анимируются дискретно. Это наиболее полезное следствие типизации. Поскольку браузер понимает, что --hue является <angle>, а не строкой, он может интерполировать между 0deg и 360deg в ходе перехода — что невозможно с нетипизированным пользовательским свойством, где браузер видит две непрозрачные строки без числовой средней точки. Спецификация CSS Transitions определяет интерполяцию как операцию над типизированными значениями; незарегистрированное пользовательское свойство не имеет типа, поэтому браузер переключает его дискретно вместо плавного перехода.

Каждое другое руководство демонстрирует это с помощью transform: rotate(). Вот более наглядный пример — анимация канала оттенка цвета oklch(), которая показывает, что типизация позволяет интерполировать значение внутри функции, а не только самостоятельное свойство:

@property --hue {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

.swatch {
  width: 200px;
  height: 200px;
  border-radius: 12px;
  background: oklch(65% 0.2 var(--hue));
  animation: hue-cycle 6s linear infinite;
}

@keyframes hue-cycle {
  to {
    --hue: 360deg;
  }
}

Образец плавно циклически проходит через весь спектр оттенков, поскольку браузер интерполирует --hue от 0deg до 360deg и пересчитывает oklch(65% 0.2 var(--hue)) на каждом кадре. Спецификация CSS Color Level 4 определяет аргумент оттенка oklch() как принимающий <angle> — именно тот тип, который мы зарегистрировали. Удалите блок @property, и анимация сломается: --hue становится нетипизированной строкой, браузер не может её интерполировать, и образец резко переключается от начала к концу вместо плавного цикла. Это сравнение «до и после» — наглядная демонстрация того, почему регистрация важна для движения.

Эквивалент на JavaScript: CSS.registerProperty()

CSS.registerProperty() является императивным эквивалентом at-правила @property. Он регистрирует типизированное пользовательское свойство во время выполнения из JavaScript, принимая объект с полями name, syntax, inherits и необязательным initialValue:

window.CSS.registerProperty({
  name: "--hue",
  syntax: "<angle>",
  inherits: false,
  initialValue: "0deg",
});

Обратите внимание на camelCase initialValue в JS API в отличие от дефисного initial-value в CSS. Справочник MDN по CSS.registerProperty() документирует имена параметров и поведение. Оба способа регистрации эквивалентны по эффекту; свойство, зарегистрированное любым из них, типизируется и валидируется одинаково.

По умолчанию используйте at-правило — оно находится вместе с остальными стилями, является декларативным и не требует выполнения JavaScript для вступления в силу. Прибегайте к CSS.registerProperty(), когда регистрация должна быть динамической: свойство, чей syntax или initialValue зависит от условий во время выполнения, или библиотека, регистрирующая свойства программно в процессе инициализации. Учтите, что свойство, зарегистрированное с помощью CSS.registerProperty(), не может быть перерегистрировано, поэтому защититесь от двойного вызова.

Поддержка браузерами

По состоянию на 9 июля 2024 года @property входит в Baseline Newly Available — поддерживается в актуальных версиях Chrome, Firefox и Safari — что делает оговорки об «экспериментальности» в более ранних материалах устаревшими. Firefox добавил поддержку в версии 128, выпущенной в июле 2024 года, что завершило кросс-браузерную поддержку; Safari реализовал её в 16.4; Chrome поддерживает её начиная с версии 85. Анонс Baseline на web.dev подтверждает дату и статус. Точные данные по версиям см. на caniuse. В руководствах, опубликованных до середины 2024 года, поддержка описывается как «экспериментальная» или «ожидаемая» — эти утверждения более не актуальны.

Практические паттерны

Наиболее ценные варианты использования @property объединяет одна черта: свойство либо анимируется, либо принимает внешние входные данные, либо требует явного управления наследованием.

Глобальное определение, локальное потребление

Определяйте блоки @property один раз в глобальном слое токенов; потребляющие компоненты ссылаются на переменную через var() как обычно. В каждом справочном руководстве @property объявляется непосредственно над использующим его правилом, что вводит в заблуждение при работе с дизайн-системами — реалистичный паттерн разделяет регистрацию и потребление:

/* tokens.css — загружается один раз в корне документа */
@property --brand-hue {
  syntax: "<angle>";
  inherits: true;
  initial-value: 250deg;
}

@property --surface {
  syntax: "<color>";
  inherits: true;
  initial-value: #1a1a1a;
}
/* card.css — компонент никогда не обращается к регистрации */
.card {
  background: var(--surface);
  border-color: oklch(60% 0.1 var(--brand-hue));
}

Любое присвоение --surface — от переключателя тем, медиазапроса или пользовательского ввода — проходит валидацию. Установка inherits: true позволяет токенам каскадироваться к потомкам.

Цветовая тематизация с адаптивными поверхностями

Типизированные цветовые токены позволяют единственному --brand-hue управлять палитрой поверхностей через oklch(); некорректное значение оттенка откатывается к зарегистрированному начальному значению вместо того, чтобы сломать палитру:

html:has(#dark:checked) {
  --surface: oklch(20% 0.1 var(--brand-hue));
}
html:has(#light:checked) {
  --surface: oklch(95% 0.04 var(--brand-hue));
}

Индикаторы прогресса на основе прокрутки

Типизированный <percentage> или <length> легко читается как значение прогресса и плавно интерполируется при управлении анимацией или обновлении из JavaScript:

@property --progress {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}

.progress-bar {
  width: var(--progress);
  transition: width 0.2s linear;
}
function onScroll() {
  const pct = (scrollY / (document.body.scrollHeight - innerHeight)) * 100;
  document.querySelector(".progress-bar")
    .style.setProperty("--progress", `${pct}%`);
}

Типизация --progress как <percentage> означает, что случайное непроцентное значение откатится к 0%, а не испортит width.

Когда не стоит использовать @property

Пропустите регистрацию @property для пользовательских свойств, которые никогда не анимируются, не принимают внешних входных данных и не требуют явного управления наследованием. Три дескриптора добавляют синтаксические накладные расходы без какой-либо выгоды во время выполнения для сугубо статических токенов. Регистрация оправдана, когда выполняется хотя бы одно из трёх условий:

  1. Значение анимируется или переходит. Интерполяция требует зарегистрированного типа.
  2. Значение принимает внешние входные данные, которые могут быть некорректными. Переключатель тем, пользовательский ввод или токен времени сборки, который может быть некорректным, выигрывает от гарантии тихого отката.
  3. Поведение наследования требует явного управления. Когда необходимо зафиксировать каскадное поведение свойства во вложенных компонентах.

Для статической шкалы отступов, уровня z-index или строки font-family, которая задаётся один раз и никогда не анимируется и не получает внешних входных данных, простое пользовательское свойство в :root проще и справляется с задачей. Добавление @property в таком случае даёт вам три дескриптора для поддержки и никакого поведения, которого у вас иначе не было бы. Типизируйте свойства, которые движутся или принимают входные данные; остальные оставляйте строками.

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

Часто задаваемые вопросы

At-правило @property является правилом верхнего уровня и объявляется самостоятельно, без вложения в :root или какой-либо селектор. Блок @property пишется в любом месте таблицы стилей для глобальной регистрации типа, а значение свойства задаётся внутри :root или любого селектора, как и для любого пользовательского свойства. Регистрация применяется ко всему документу независимо от того, где впоследствии присваивается значение, поэтому стандартный паттерн — размещение @property в глобальном файле токенов и присвоение значений в :root.

Оба способа регистрируют типизированное пользовательское свойство с идентичным поведением во время выполнения, но @property — это декларативный CSS, вступающий в силу без JavaScript, тогда как CSS.registerProperty() выполняется императивно во время выполнения. По умолчанию используйте @property, поскольку он находится вместе со стилями и не требует выполнения скрипта. Прибегайте к CSS.registerProperty() только тогда, когда регистрация должна быть динамической, например, когда syntax или initialValue зависят от условий во время выполнения. Учтите, что CSS.registerProperty() использует camelCase initialValue, не может перерегистрировать свойство и выбрасывает исключение при повторном вызове для того же имени.

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

Браузер отбрасывает некорректное присвоение и отображает элемент, используя зарегистрированный initial-value. Это происходит молча, без ошибки в консоли и без визуального индикатора на отображаемой странице о срабатывании отката — поведение, описанное в спецификации как становление недействительным во время вычисления значения. Страница не ломается, но и не сигнализирует о том, что что-то пошло не так, что делает подобные регрессии невидимыми для стандартного мониторинга ошибок. DevTools показывает вычисленное резервное значение только при непосредственном инспектировании элемента.

Truly understand users experience

See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data. . Check our GitHub repo and join the thousands of developers in our community..

OpenReplay