Travailler avec des variables CSS typées grâce à @property
La règle at-rule CSS @property attribue un type à une propriété personnalisée. Une fois enregistrée, le navigateur valide chaque affectation, interpole entre les valeurs lors des animations et revient à une valeur initiale définie en cas d’entrée invalide. @property comble les lacunes de validation et d’interpolation des variables CSS — mais il modifie le mode d’échec d’une manière que la plupart des développeurs n’anticipent pas, en remplaçant une valeur visiblement incorrecte par un repli silencieux qui ne génère aucune erreur.
Cet article couvre les trois descripteurs et ceux qui sont obligatoires, le registre de types pris en charge avec des exemples concrets, le comportement de repli silencieux et ce que les utilisateurs voient réellement lorsqu’il se déclenche, l’animation de propriétés typées au-delà de la démonstration de rotation habituelle, l’équivalent JavaScript CSS.registerProperty(), la prise en charge actuelle par les navigateurs, et un critère de décision pour savoir quand l’enregistrement ne vaut pas la complexité qu’il implique.
Points clés à retenir
- Une propriété personnalisée CSS non typée est une chaîne de caractères ;
@propertylui confère un type que le navigateur valide à chaque point d’affectation. - Les descripteurs
syntaxetinheritssont toujours obligatoires, etinitial-valueest requis dès lors quesyntaxn’est pas"*"— conformément à la spécification CSS Properties and Values API Level 1. - Lorsqu’une propriété enregistrée reçoit une valeur ne correspondant pas à sa
syntaxdéclarée, le navigateur revient silencieusement àinitial-value, sans erreur dans la console et sans indicateur visuel signalant que le repli s’est déclenché. - Les propriétés typées interpolent lors des transitions et des animations ; les propriétés personnalisées non typées ne le font pas, car le navigateur voit deux chaînes opaques sans valeur numérique intermédiaire.
@propertyest disponible en Baseline depuis le 9 juillet 2024 — les mises en garde « expérimental » présentes dans les références antérieures à 2024 sont obsolètes.
Le problème : les propriétés personnalisées non typées ne sont que des chaînes de caractères
Une propriété personnalisée CSS standard contient une chaîne non analysée jusqu’à ce qu’elle soit substituée dans une propriété réelle. Le navigateur ne sait pas si --accent est censé être une couleur, une longueur ou un mot-clé. Il n’effectue aucune validation au point de déclaration, ne peut pas interpoler entre deux valeurs lors d’une animation, et ne vous fournit aucun retour lorsqu’une valeur est structurellement incorrecte par rapport à l’usage que vous en prévoyez.
Cette troisième lacune est celle qui pose le plus de problèmes en pratique. Considérons une propriété non typée utilisée dans un text-shadow :
.card {
--accent: red;
text-shadow: 4px 2px 5px var(--accent);
}
/* ailleurs, par erreur */
.card {
--accent: 20px;
}
La déclaration text-shadow devient invalide au moment de la substitution et l’ombre disparaît. Aucun avertissement n’est émis à l’endroit où --accent a été défini à 20px, car à ce stade il s’agit toujours d’une simple chaîne. Le navigateur n’a aucune notion que cette propriété était censée être une couleur. Le guide MDN sur les propriétés personnalisées décrit ce modèle de substitution : la valeur d’une propriété personnalisée n’est résolue que lorsqu’elle est référencée via var().
@property, issu de la spécification CSS Properties and Values API Level 1, ajoute un type à la propriété elle-même. Une fois enregistré, le navigateur sait que --accent est une <color> et l’impose à chaque affectation, et non uniquement lors de la substitution.
Syntaxe : les trois descripteurs et ceux qui sont obligatoires
Discover how at OpenReplay.com.
La règle at-rule @property accepte trois descripteurs : syntax, inherits et initial-value. Les descripteurs syntax et inherits sont toujours obligatoires ; initial-value est requis dès lors que syntax n’est pas "*" — l’omettre dans un enregistrement typé rend l’intégralité du bloc @property invalide et ignoré.
@property --accent {
syntax: "<color>";
inherits: false;
initial-value: #586de7;
}
syntax— une chaîne décrivant le type accepté, tirée d’un ensemble fixe de noms pris en charge définis par la spécification (abordés dans la section suivante).inherits— un booléen (trueoufalse) contrôlant si la propriété hérite vers le bas de l’arbre DOM. Il s’agit du même comportement d’héritage que possède toute propriété CSS ; le spécifier explicitement est ce qui rend les propriétés typées prévisibles au sein des composants imbriqués.initial-value— la valeur utilisée lorsqu’aucune autre valeur valide ne s’applique, et la valeur vers laquelle la propriété se replie en cas d’entrée invalide.
La spécification CSS Properties and Values API Level 1, §3.1 définit précisément cette exigence : une règle @property est invalide si syntax ou inherits est absent, et elle est invalide si initial-value est absent sauf si syntax est le sélecteur universel "*". Plusieurs tutoriels existants décrivent initial-value comme inconditionnellement requis ; la spécification le lie à la valeur de syntax, et inherits n’a aucune incidence sur cette condition. Une règle @property invalide est ignorée — l’enregistrement n’a tout simplement pas lieu, et la propriété revient à un comportement non typé.
Le registre de types CSS @property
Le descripteur syntax accepte les noms de composants syntaxiques pris en charge définis par la spécification CSS Properties and Values API Level 1, §2 — notamment <color>, <length>, <percentage>, <integer>, <angle>, <image> et <custom-ident> — ainsi que des multiplicateurs (+ pour les listes séparées par des espaces, # pour les listes séparées par des virgules) et la syntaxe d’union (<color> | <length>) pour les propriétés qui acceptent légitimement plusieurs types. Il s’agit d’une liste fixe de noms pris en charge, et non d’un accès libre à n’importe quel mot-clé de type CSS.
Valeur syntax | Accepte | Rejette | Exemple d’initial-value |
|---|---|---|---|
"<color>" | toute couleur valide (#f00, rebeccapurple, oklch(...)) | longueurs, mots-clés comme darkpink | #586de7 |
"<length>" | px, rem, em, vw, etc. | nombres nus, pourcentages | 20px |
"<percentage>" | 50% | longueurs, nombres nus | 100% |
"<integer>" | nombres entiers (12) | 1.5, longueurs | 12 |
"<angle>" | deg, rad, turn, grad | nombres nus | 0deg |
"<image>" | url(...), dégradés | couleurs, longueurs | url(bg.png) |
"<custom-ident>" | identifiants définis par l’auteur | nombres, chaînes entre guillemets | none |
"*" | toute valeur (passage non typé) | rien — accepte tout | optionnel |
Trois extensions grammaticales élargissent ce qu’une seule propriété peut accepter :
/* "+" — une liste de longueurs séparées par des espaces */
@property --insets {
syntax: "<length>+";
inherits: false;
initial-value: 0px;
}
/* "#" — une liste de couleurs séparées par des virgules */
@property --stops {
syntax: "<color>#";
inherits: false;
initial-value: black;
}
/* "|" — une union : accepte soit une longueur, soit le mot-clé "auto" */
@property --gap {
syntax: "<length> | auto";
inherits: false;
initial-value: auto;
}
Le multiplicateur + désigne une liste séparée par des espaces ; # désigne une liste séparée par des virgules, conformément à la section sur les chaînes syntaxiques prises en charge de la spécification. L’union | permet à une propriété d’accepter plusieurs types — utile pour les propriétés qui acceptent réellement, par exemple, une longueur ou un mot-clé. La syntaxe universelle "*" désactive entièrement la vérification de type ; c’est le seul cas où initial-value est optionnel, car il n’y a pas de type pour lequel définir une valeur par défaut. Pour les définitions par type, la référence MDN sur les types de valeurs CSS et l’index des types CSSWG listent chaque nom de composant.
Validation : le repli silencieux que vous n’anticipez pas
Lorsqu’une propriété enregistrée reçoit une valeur ne correspondant pas à sa syntax déclarée, le navigateur ignore l’affectation et rend l’élément en utilisant initial-value. Dans les navigateurs actuels, ce repli ne génère aucune erreur dans la console et aucune indication visuelle dans la page rendue signalant que le repli s’est déclenché — la page ne se casse pas, mais elle ne vous informe pas non plus que quelque chose a mal tourné.
@property --hue {
syntax: "<angle>";
inherits: false;
initial-value: 90deg;
}
.card {
--hue: 220deg; /* ✅ valide, utilisé */
--hue: #f00; /* ❌ type invalide, ignoré — revient à 90deg */
background: oklch(70% 0.15 var(--hue));
}
Le background se résout toujours en une couleur valide. Après l’affectation invalide, --hue ne devient pas #f00 et ne devient pas vide — la valeur invalide est ignorée et la propriété se résout à son initial-value enregistré, soit 90deg. La page MDN sur @property documente ce comportement comme la propriété devenant « invalide au moment de la valeur calculée », ce qui se résout à la valeur initiale enregistrée.
C’est indéniablement préférable à une mise en page visiblement cassée. C’est aussi une nouvelle catégorie d’échec. Les propriétés personnalisées non typées échouent de manière visible — la déclaration dépendante se casse et vous le constatez. Les propriétés typées échouent silencieusement : un sélecteur de thème JS écrit une couleur malformée, une valeur fournie par l’utilisateur ne se parse pas, un token de design reçoit la mauvaise unité, et le composant s’affiche dans son état par défaut sans aucune trace d’erreur. Les DevTools affichent la valeur de repli calculée si vous inspectez l’élément, mais rien ne remonte dans la console à l’exécution.
C’est précisément ce type de bug que le session replay est conçu pour détecter. Lorsqu’une propriété personnalisée typée reçoit une entrée invalide en production — provenant d’une saisie utilisateur, d’un token mal configuré ou d’un changement de thème à l’exécution — le navigateur revient silencieusement à initial-value, aucune erreur JavaScript n’est levée, et la surveillance d’erreurs standard ne produit aucun signal. La seule preuve post-déploiement est visuelle : un composant affiché avec la mauvaise couleur ou la mauvaise taille. Les replays de session de ces implémentations révèlent fréquemment l’état incorrect directement, en capturant le DOM rendu au moment où la mauvaise valeur a été affectée, là où un outil limité à la console ne voit rien.
Animation : interpolation des propriétés typées
Les propriétés personnalisées enregistrées interpolent lors des animations ; les propriétés non enregistrées s’animent de manière discrète. C’est la conséquence la plus utile du typage. Parce que le navigateur comprend que --hue est un <angle> et non une chaîne, il peut interpoler entre 0deg et 360deg au cours d’une transition — ce qui est impossible avec une propriété personnalisée non typée, où le navigateur voit deux chaînes opaques sans valeur numérique intermédiaire. La spécification CSS Transitions définit l’interpolation comme opérant sur des valeurs typées ; une propriété personnalisée non enregistrée n’a pas de type, donc le navigateur la permute de manière discrète au lieu de l’interpoler progressivement.
Tous les autres tutoriels illustrent cela avec transform: rotate(). Voici un cas plus parlant — animer le canal de teinte d’une couleur oklch(), ce qui montre que le typage permet d’interpoler une valeur à l’intérieur d’une fonction, et pas seulement une propriété autonome :
@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;
}
}
L’échantillon de couleur parcourt en douceur la totalité de la roue chromatique car le navigateur interpole --hue de 0deg à 360deg et recalcule oklch(65% 0.2 var(--hue)) à chaque image. La spécification CSS Color Level 4 définit l’argument de teinte de oklch() comme acceptant un <angle>, ce qui correspond exactement à ce que nous avons enregistré. Supprimez le bloc @property et l’animation se casse : --hue devient une chaîne non typée, le navigateur ne peut plus l’interpoler, et l’échantillon passe brusquement du début à la fin au lieu de cycler progressivement. Cette comparaison avant/après est la démonstration la plus claire de l’importance de l’enregistrement pour le mouvement.
L’équivalent JavaScript : CSS.registerProperty()
CSS.registerProperty() est l’équivalent impératif de la règle at-rule @property. Il enregistre une propriété personnalisée typée à l’exécution depuis JavaScript, en prenant un objet avec name, syntax, inherits et un initialValue optionnel :
window.CSS.registerProperty({
name: "--hue",
syntax: "<angle>",
inherits: false,
initialValue: "0deg",
});
Notez le initialValue en camelCase dans l’API JS, par opposition au descripteur initial-value avec tiret en CSS. La référence MDN sur CSS.registerProperty() documente les noms des paramètres et le comportement. Les deux méthodes d’enregistrement sont équivalentes en termes d’effet ; une propriété enregistrée de l’une ou l’autre manière est typée et validée de façon identique.
Privilégiez par défaut la règle at-rule — elle coexiste avec le reste de vos styles, est déclarative et ne nécessite aucune exécution JavaScript pour prendre effet. Utilisez CSS.registerProperty() lorsque l’enregistrement doit être dynamique : une propriété dont la syntax ou l’initialValue dépend de conditions à l’exécution, ou une bibliothèque qui enregistre des propriétés de manière programmatique dans le cadre de son initialisation. Notez qu’une propriété enregistrée avec CSS.registerProperty() ne peut pas être ré-enregistrée, donc protégez-vous contre une double exécution.
Prise en charge par les navigateurs
Depuis le 9 juillet 2024, @property est disponible en Baseline — pris en charge dans les versions actuelles de Chrome, Firefox et Safari — rendant obsolètes les mises en garde « expérimental » présentes dans les références plus anciennes. Firefox a ajouté la prise en charge dans la version 128, publiée en juillet 2024, ce qui a complété la prise en charge cross-navigateur ; Safari l’a intégré dans la version 16.4 ; Chrome le prend en charge depuis la version 85. L’annonce Baseline sur web.dev confirme la date et le statut. Pour les données de versions exactes, consultez caniuse. Les tutoriels publiés avant mi-2024 décrivent la prise en charge comme « expérimentale » ou « à venir » ; ces affirmations ne sont plus d’actualité.
Cas d’usage concrets
Les utilisations les plus pertinentes de @property partagent une caractéristique commune : la propriété est soit animée, soit soumise à des entrées externes, soit nécessite un contrôle explicite de l’héritage.
Définition globale, consommation locale
Définissez les blocs @property une seule fois dans une couche de tokens globale ; les composants consommateurs référencent la variable avec var() comme d’habitude. Chaque tutoriel de référence déclare @property directement au-dessus de la règle qui l’utilise, ce qui est trompeur pour le travail sur les design systems — le modèle réaliste sépare l’enregistrement de la consommation :
/* tokens.css — chargé une seule fois à la racine du document */
@property --brand-hue {
syntax: "<angle>";
inherits: true;
initial-value: 250deg;
}
@property --surface {
syntax: "<color>";
inherits: true;
initial-value: #1a1a1a;
}
/* card.css — le composant ne fait jamais référence à l'enregistrement */
.card {
background: var(--surface);
border-color: oklch(60% 0.1 var(--brand-hue));
}
Toute affectation à --surface — depuis un sélecteur de thème, une media query ou une saisie utilisateur — est validée. Définir inherits: true permet aux tokens de se cascader vers les descendants.
Thématisation des couleurs avec des surfaces adaptatives
Les tokens de couleur typés permettent à un seul --brand-hue de piloter une palette de surfaces via oklch() ; une valeur de teinte malformée revient à la valeur initiale enregistrée au lieu de casser la palette :
html:has(#dark:checked) {
--surface: oklch(20% 0.1 var(--brand-hue));
}
html:has(#light:checked) {
--surface: oklch(95% 0.04 var(--brand-hue));
}
Indicateurs de progression pilotés par le défilement
Un <percentage> ou une <length> typé se lit clairement comme une valeur de progression et interpole en douceur lorsqu’il est piloté par une animation ou mis à jour depuis 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}%`);
}
Typer --progress en <percentage> signifie qu’une valeur non-pourcentage parasite revient à 0% plutôt que de corrompre width.
Quand ne pas utiliser @property
Évitez l’enregistrement @property pour les propriétés personnalisées qui ne s’animent jamais, ne reçoivent jamais d’entrées externes et ne nécessitent pas de contrôle explicite de l’héritage. Les trois descripteurs ajoutent une surcharge syntaxique sans bénéfice à l’exécution pour des tokens purement statiques. L’enregistrement se justifie lorsqu’au moins l’une des trois conditions suivantes est remplie :
- La valeur s’anime ou effectue une transition. L’interpolation nécessite un type enregistré.
- La valeur reçoit des entrées externes potentiellement invalides. Un sélecteur de thème, une saisie utilisateur ou un token de build qui pourrait être malformé bénéficie de la garantie de repli silencieux.
- Le comportement d’héritage nécessite un contrôle explicite. Lorsque vous devez verrouiller le comportement de cascade d’une propriété au sein de composants imbriqués.
Pour une échelle d’espacement statique, un niveau de z-index ou une chaîne font-family définie une seule fois et jamais animée ni alimentée par des entrées externes, une propriété personnalisée simple dans :root est plus simple et fait le travail. Ajouter @property dans ce cas vous impose trois descripteurs à maintenir sans aucun comportement supplémentaire. Typez les propriétés qui s’animent ou reçoivent des entrées ; laissez les autres sous forme de chaînes.
Les propriétés personnalisées typées transforment le navigateur en validateur et en moteur d’interpolation, mais la contrepartie est un mode d’échec silencieux : les entrées invalides reviennent à initial-value sans aucune trace d’erreur. Enregistrez les propriétés qui s’animent ou reçoivent des entrées à l’exécution, définissez des valeurs initiales sensées, et traitez ce repli comme un comportement à surveiller en production plutôt que comme un filet de sécurité qui masque les problèmes.
FAQ
La règle at-rule @property est une règle at-rule de premier niveau et se déclare de manière autonome, sans être imbriquée dans :root ou dans un sélecteur quelconque. Vous écrivez le bloc @property n'importe où dans votre feuille de style pour enregistrer le type globalement, puis vous définissez la valeur de la propriété dans :root ou dans n'importe quel sélecteur comme vous le feriez pour toute propriété personnalisée. L'enregistrement s'applique à l'ensemble du document, quel que soit l'endroit où la valeur est ensuite affectée ; placer @property dans un fichier de tokens global et affecter les valeurs dans :root est le modèle standard.
Les deux enregistrent une propriété personnalisée typée avec un comportement à l'exécution identique, mais @property est du CSS déclaratif qui prend effet sans JavaScript, tandis que CSS.registerProperty() s'exécute de manière impérative à l'exécution. Utilisez @property par défaut, car il coexiste avec vos styles et ne nécessite aucune exécution de script. Recourez à CSS.registerProperty() uniquement lorsque l'enregistrement doit être dynamique, par exemple lorsque syntax ou initialValue dépend de conditions à l'exécution. Notez que CSS.registerProperty() utilise initialValue en camelCase, ne peut pas ré-enregistrer une propriété et lève une exception si appelé deux fois pour le même nom.
Non. Une propriété personnalisée non typée est stockée comme une chaîne opaque, donc le navigateur voit deux chaînes sans valeur numérique intermédiaire et permute la valeur de manière discrète au lieu de l'interpoler. Enregistrer la propriété avec @property lui confère un type que le navigateur comprend, ce qui permet l'interpolation lors des transitions et des keyframes. Par exemple, un angle non enregistré passe brusquement du début à la fin, tandis que la même propriété enregistrée comme <angle> interpole en douceur. L'enregistrement du type est ce qui rend l'interpolation possible.
Le navigateur ignore l'affectation invalide et rend l'élément en utilisant la valeur initial-value enregistrée. Cela se produit silencieusement, sans erreur dans la console et sans indication visuelle dans la page rendue que le repli s'est déclenché — comportement décrit dans la spécification comme devenant invalide au moment de la valeur calculée. La page ne se casse pas, mais elle ne signale pas non plus que quelque chose a mal tourné, ce qui rend ces régressions invisibles pour la surveillance d'erreurs standard. Les DevTools affichent la valeur de repli calculée uniquement si vous inspectez l'élément directement.
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..