Back

Arbeiten mit typisierten CSS-Variablen mittels @property

Arbeiten mit typisierten CSS-Variablen mittels @property

Die CSS-At-Regel @property weist einer benutzerdefinierten Eigenschaft einen Typ zu. Nach der Registrierung validiert der Browser jede Zuweisung, interpoliert zwischen Werten während Animationen und greift bei ungültiger Eingabe auf einen definierten Anfangswert zurück. @property schließt die Validierungs- und Interpolationslücken von CSS-Variablen – ändert jedoch das Fehlerverhalten auf eine Weise, die die meisten Entwickler nicht erwarten: Ein sichtbar fehlerhafter Wert wird durch einen stillen Fallback ersetzt, der keinen Fehler auslöst.

Dieser Artikel behandelt die drei Deskriptoren und welche davon erforderlich sind, die unterstützte Typ-Registry mit konkreten Beispielen, das stille Fallback-Verhalten und was Benutzer tatsächlich sehen, wenn es ausgelöst wird, die Animation typisierter Eigenschaften jenseits der üblichen Rotationsdemonstration, das JavaScript-Äquivalent CSS.registerProperty(), die aktuelle Browser-Unterstützung sowie ein Entscheidungskriterium dafür, wann eine Registrierung den Aufwand nicht rechtfertigt.

Wichtige Erkenntnisse

  • Eine untypisierte benutzerdefinierte CSS-Eigenschaft ist ein String; @property gibt ihr einen Typ, den der Browser bei jeder Zuweisung validiert.
  • Die Deskriptoren syntax und inherits sind immer erforderlich, und initial-value ist immer dann erforderlich, wenn syntax nicht "*" ist – gemäß der Spezifikation CSS Properties and Values API Level 1.
  • Wenn eine registrierte Eigenschaft einen Wert erhält, der nicht ihrer deklarierten syntax entspricht, setzt der Browser stillschweigend auf initial-value zurück – ohne Konsolenfehler und ohne visuellen Hinweis, dass der Fallback ausgelöst wurde.
  • Typisierte Eigenschaften interpolieren während Übergängen und Animationen; untypisierte benutzerdefinierte Eigenschaften tun dies nicht, da der Browser zwei undurchsichtige Strings ohne numerischen Mittelpunkt sieht.
  • @property ist seit dem 9. Juli 2024 als Baseline Newly Available verfügbar – die Hinweise auf „experimentellen” Status in Referenzen vor 2024 sind überholt.

Das Problem: Untypisierte benutzerdefinierte Eigenschaften sind nur Strings

Eine standardmäßige benutzerdefinierte CSS-Eigenschaft enthält einen nicht geparsten String, bis dieser in eine echte Eigenschaft substituiert wird. Der Browser weiß nicht, ob --accent eine Farbe, eine Länge oder ein Schlüsselwort sein soll. Er führt keine Validierung an der Deklarationsstelle durch, kann während einer Animation nicht zwischen zwei Werten interpolieren und gibt kein Feedback, wenn ein Wert strukturell falsch für den beabsichtigten Verwendungszweck ist.

Diese dritte Lücke ist die praktisch relevante. Betrachten wir eine untypisierte Eigenschaft in einem text-shadow:

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

/* irgendwo anders, versehentlich */
.card {
  --accent: 20px;
}

Die text-shadow-Deklaration wird zum Zeitpunkt der Substitution ungültig und der Schatten verschwindet. An der Stelle, wo --accent auf 20px gesetzt wurde, gibt es keine Warnung, da es zu diesem Zeitpunkt noch ein einfacher String ist. Der Browser hat keine Vorstellung davon, dass diese Eigenschaft eine Farbe sein sollte. Der MDN-Leitfaden zu benutzerdefinierten Eigenschaften beschreibt dieses Substitutionsmodell: Der Wert einer benutzerdefinierten Eigenschaft wird erst aufgelöst, wenn er über var() referenziert wird.

@property aus der Spezifikation CSS Properties and Values API Level 1 fügt der Eigenschaft selbst einen Typ hinzu. Nach der Registrierung weiß der Browser, dass --accent eine <color> ist, und erzwingt dies bei jeder Zuweisung – nicht erst bei der Substitution.

Syntax: Die drei Deskriptoren und welche erforderlich sind

Die At-Regel @property nimmt drei Deskriptoren entgegen: syntax, inherits und initial-value. Die Deskriptoren syntax und inherits sind immer erforderlich; initial-value ist immer dann erforderlich, wenn syntax nicht "*" ist – wird er bei einer typisierten Registrierung weggelassen, wird der gesamte @property-Block als ungültig betrachtet und ignoriert.

@property --accent {
  syntax: "<color>";
  inherits: false;
  initial-value: #586de7;
}
  • syntax — ein String, der den akzeptierten Typ beschreibt, entnommen aus einer festen Menge unterstützter Namen, die durch die Spezifikation definiert sind (im nächsten Abschnitt behandelt).
  • inherits — ein boolescher Wert (true oder false), der steuert, ob die Eigenschaft im DOM-Baum nach unten vererbt wird. Dies entspricht dem Vererbungsverhalten jeder CSS-Eigenschaft; die explizite Angabe macht typisierte Eigenschaften über verschachtelte Komponenten hinweg vorhersagbar.
  • initial-value — der Wert, der verwendet wird, wenn kein anderer gültiger Wert gilt, und der Wert, auf den die Eigenschaft bei ungültiger Eingabe zurückfällt.

Der Abschnitt §3.1 der CSS Properties and Values API Level 1-Spezifikation definiert die Anforderung präzise: Eine @property-Regel ist ungültig, wenn syntax oder inherits fehlt, und sie ist ungültig, wenn initial-value fehlt – es sei denn, syntax ist das universelle "*". Einige bestehende Tutorials beschreiben initial-value als bedingungslos erforderlich; die Spezifikation knüpft dies an den syntax-Wert, und inherits hat keinen Einfluss auf diese Bedingung. Eine ungültige @property-Regel wird verworfen – die Registrierung findet schlicht nicht statt, und die Eigenschaft kehrt zum untypisierten Verhalten zurück.

Die CSS-@property-Typ-Registry

Der syntax-Deskriptor akzeptiert die unterstützten Syntaxkomponentennamen, die durch den Abschnitt §2 der CSS Properties and Values API Level 1-Spezifikation definiert sind – darunter <color>, <length>, <percentage>, <integer>, <angle>, <image> und <custom-ident> – sowie Multiplikatoren (+ für leerzeichengetrennte Listen, # für kommagetrennte Listen) und Union-Syntax (<color> | <length>) für Eigenschaften, die legitim mehrere Typen akzeptieren. Dies ist eine feste Liste unterstützter Namen und keine offene Tür für beliebige CSS-Typ-Schlüsselwörter.

syntax-WertAkzeptiertLehnt abBeispiel initial-value
"<color>"Jede gültige Farbe (#f00, rebeccapurple, oklch(...))Längen, Schlüsselwörter wie darkpink#586de7
"<length>"px, rem, em, vw usw.Bare Zahlen, Prozentangaben20px
"<percentage>"50%Längen, bare Zahlen100%
"<integer>"Ganze Zahlen (12)1.5, Längen12
"<angle>"deg, rad, turn, gradBare Zahlen0deg
"<image>"url(...), VerläufeFarben, Längenurl(bg.png)
"<custom-ident>"Benutzerdefinierte BezeichnerZahlen, Strings mit Anführungszeichennone
"*"Beliebiger Wert (untypisierter Durchlauf)Nichts – akzeptiert allesoptional

Drei Grammatikerweiterungen erweitern, was eine einzelne Eigenschaft akzeptiert:

/* "+" — eine leerzeichengetrennte Liste von Längen */
@property --insets {
  syntax: "<length>+";
  inherits: false;
  initial-value: 0px;
}

/* "#" — eine kommagetrennte Liste von Farben */
@property --stops {
  syntax: "<color>#";
  inherits: false;
  initial-value: black;
}

/* "|" — eine Union: akzeptiert entweder eine Länge oder das Schlüsselwort "auto" */
@property --gap {
  syntax: "<length> | auto";
  inherits: false;
  initial-value: auto;
}

Der Multiplikator + bezeichnet eine leerzeichengetrennte Liste; # bezeichnet eine kommagetrennte Liste, gemäß dem Abschnitt zu unterstützten Syntaxstrings der Spezifikation. Die Union | ermöglicht es einer Eigenschaft, mehr als einen Typ zu akzeptieren – nützlich für Eigenschaften, die tatsächlich beispielsweise eine Länge oder ein Schlüsselwort entgegennehmen. Die universelle Syntax "*" deaktiviert die Typprüfung vollständig; dies ist der einzige Fall, in dem initial-value optional ist, da kein Typ als Standard dienen kann. Für typspezifische Definitionen listen die MDN-Referenz zu CSS-Werttypen und der CSSWG-Typenindex jeden Komponentennamen auf.

Validierung: Der stille Fallback, den man nicht erwartet

Wenn eine registrierte Eigenschaft einen Wert erhält, der nicht ihrer deklarierten syntax entspricht, verwirft der Browser die Zuweisung und rendert das Element unter Verwendung von initial-value. In aktuellen Browsern erzeugt dieser Fallback weder einen Konsolenfehler noch einen visuellen Hinweis auf der gerenderten Seite, dass der Fallback ausgelöst wurde – die Seite bricht nicht, teilt aber auch nicht mit, dass etwas schiefgelaufen ist.

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

.card {
  --hue: 220deg;            /* ✅ gültig, wird verwendet */
  --hue: #f00;              /* ❌ ungültiger Typ, wird ignoriert — fällt auf 90deg zurück */
  background: oklch(70% 0.15 var(--hue));
}

Der background wird immer zu einer gültigen Farbe aufgelöst. Nach der ungültigen Zuweisung wird --hue weder zu #f00 noch leer – der ungültige Wert wird verworfen und die Eigenschaft löst sich zu ihrem registrierten initial-value von 90deg auf. Die MDN-Seite zu @property dokumentiert dies als Eigenschaft, die „zum Zeitpunkt des berechneten Werts ungültig wird” und sich zum registrierten Anfangswert auflöst.

Dies ist zweifellos besser als ein sichtbar fehlerhaftes Layout. Es handelt sich jedoch auch um eine neue Fehlerklasse. Untypisierte benutzerdefinierte Eigenschaften scheitern lautstark – die abhängige Deklaration bricht und man sieht es. Typisierte Eigenschaften scheitern leise: Ein JS-Theme-Switcher schreibt eine fehlerhafte Farbe, ein benutzerseitig bereitgestellter Wert lässt sich nicht parsen, ein Design-Token erhält die falsche Einheit – und die Komponente rendert in ihrem Standardzustand ohne jede Fehlerspur. DevTools zeigt den berechneten Fallback-Wert an, wenn man das Element inspiziert, aber zur Laufzeit erscheint nichts in der Konsole.

Dies ist genau die Art von Fehler, für die Session-Replay entwickelt wurde. Wenn eine typisierte benutzerdefinierte Eigenschaft in der Produktion eine ungültige Eingabe erhält – durch Benutzereingaben, ein falsch konfiguriertes Token oder eine Laufzeit-Themenänderung – fällt der Browser stillschweigend auf initial-value zurück, es wird kein JavaScript-Fehler ausgelöst, und das Standard-Fehlermonitoring liefert kein Signal. Der einzige nachträgliche Hinweis ist visueller Natur: eine Komponente, die in der falschen Farbe oder Größe gerendert wird. Session-Replays solcher Implementierungen zeigen häufig den Fehlzustand direkt, indem sie das gerenderte DOM zum Zeitpunkt der fehlerhaften Wertzuweisung erfassen – wo ein reines Konsolen-Tool nichts sieht.

Animation: Interpolation typisierter Eigenschaften

Registrierte benutzerdefinierte Eigenschaften interpolieren während Animationen; nicht registrierte animieren stattdessen diskret. Dies ist die praktisch bedeutsamste Konsequenz der Typisierung. Da der Browser versteht, dass --hue ein <angle> und kein String ist, kann er während eines Übergangs zwischen 0deg und 360deg interpolieren – etwas, das mit einer untypisierten benutzerdefinierten Eigenschaft unmöglich ist, da der Browser dort zwei undurchsichtige Strings ohne numerischen Mittelpunkt sieht. Die CSS-Transitions-Spezifikation definiert Interpolation als auf typisierten Werten operierend; eine nicht registrierte benutzerdefinierte Eigenschaft hat keinen Typ, weshalb der Browser sie diskret wechselt statt zu tweenen.

Jedes andere Tutorial demonstriert dies mit transform: rotate(). Hier ist ein anschaulicherer Fall – die Animation des Farbton-Kanals einer oklch()-Farbe, der zeigt, dass Typisierung die Interpolation eines Werts innerhalb einer Funktion ermöglicht, nicht nur einer eigenständigen Eigenschaft:

@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;
  }
}

Das Farbfeld durchläuft das gesamte Farbspektrum gleichmäßig, weil der Browser --hue von 0deg nach 360deg tweent und oklch(65% 0.2 var(--hue)) bei jedem Frame neu berechnet. Die CSS Color Level 4-Spezifikation definiert das Farbton-Argument von oklch() als <angle> akzeptierend – genau das, was wir registriert haben. Entfernt man den @property-Block, bricht die Animation: --hue wird zu einem untypisierten String, der Browser kann ihn nicht interpolieren, und das Farbfeld springt vom Anfangs- zum Endwert, anstatt gleichmäßig zu wechseln. Dieses Vorher-Nachher-Verhalten ist die deutlichste Demonstration dafür, warum Registrierung für Bewegungseffekte wichtig ist.

Das JavaScript-Äquivalent: CSS.registerProperty()

CSS.registerProperty() ist das imperative Äquivalent der At-Regel @property. Es registriert zur Laufzeit aus JavaScript heraus eine typisierte benutzerdefinierte Eigenschaft und nimmt ein Objekt mit name, syntax, inherits und einem optionalen initialValue entgegen:

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

Beachten Sie das camelCase initialValue in der JS-API gegenüber dem mit Bindestrich geschriebenen initial-value-Deskriptor in CSS. Die MDN-Referenz zu CSS.registerProperty() dokumentiert die Parameternamen und das Verhalten. Beide Registrierungswege sind in ihrer Wirkung gleichwertig; eine auf einem der beiden Wege registrierte Eigenschaft wird identisch typisiert und validiert.

Verwenden Sie standardmäßig die At-Regel – sie lebt zusammen mit dem Rest Ihrer Styles, ist deklarativ und erfordert keine JavaScript-Ausführung, um wirksam zu sein. Greifen Sie auf CSS.registerProperty() zurück, wenn die Registrierung dynamisch erfolgen muss: bei einer Eigenschaft, deren syntax oder initialValue von Laufzeitbedingungen abhängt, oder bei einer Bibliothek, die Eigenschaften programmatisch als Teil ihrer Initialisierung registriert. Beachten Sie, dass eine mit CSS.registerProperty() registrierte Eigenschaft nicht erneut registriert werden kann – schützen Sie sich daher davor, es zweimal aufzurufen.

Browser-Unterstützung

Seit dem 9. Juli 2024 ist @property als Baseline Newly Available verfügbar – unterstützt in aktuellen Versionen von Chrome, Firefox und Safari – womit die Hinweise auf „experimentellen” Status in älteren Referenzen überholt sind. Firefox fügte die Unterstützung in Version 128 hinzu, veröffentlicht im Juli 2024, womit die browserübergreifende Unterstützung vollständig wurde; Safari lieferte sie in Version 16.4; Chrome unterstützt es seit Version 85. Die web.dev Baseline-Ankündigung bestätigt Datum und Status. Für genaue Versionsdaten siehe caniuse. Tutorials, die vor Mitte 2024 veröffentlicht wurden, beschreiben die Unterstützung als „experimentell” oder „bevorstehend” – diese Aussagen gelten nicht mehr.

Praxisnahe Muster

Die wertvollsten Anwendungsfälle von @property teilen ein gemeinsames Merkmal: Die Eigenschaft animiert, nimmt externe Eingaben entgegen oder benötigt explizite Vererbungssteuerung.

Globale Definition, bereichsbezogener Einsatz

Definieren Sie @property-Blöcke einmalig in einer globalen Token-Schicht; konsumierende Komponenten referenzieren die Variable wie gewohnt mit var(). Jedes Referenz-Tutorial deklariert @property direkt oberhalb der Regel, die es verwendet – was für die Arbeit mit Design-Systemen irreführend ist. Das realistische Muster trennt Registrierung von Verwendung:

/* tokens.css — einmalig am Dokumentstamm geladen */
@property --brand-hue {
  syntax: "<angle>";
  inherits: true;
  initial-value: 250deg;
}

@property --surface {
  syntax: "<color>";
  inherits: true;
  initial-value: #1a1a1a;
}
/* card.css — die Komponente referenziert die Registrierung nie direkt */
.card {
  background: var(--surface);
  border-color: oklch(60% 0.1 var(--brand-hue));
}

Jede Zuweisung an --surface – von einem Theme-Switcher, einer Media Query oder einer Benutzereingabe – wird validiert. Das Setzen von inherits: true lässt die Tokens zu Nachfahren kaskadieren.

Farbgebung mit adaptiven Oberflächen

Typisierte Farb-Tokens ermöglichen es, mit einem einzigen --brand-hue eine Oberflächenpalette über oklch() zu steuern; ein fehlerhafter Farbton-Wert fällt auf den registrierten Anfangswert zurück, anstatt die Palette zu beschädigen:

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

Scroll-gesteuerte Fortschrittsanzeigen

Ein typisierter <percentage>- oder <length>-Wert lässt sich sauber als Fortschrittswert lesen und interpoliert gleichmäßig, wenn er durch eine Animation gesteuert oder aus JavaScript aktualisiert wird:

@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}%`);
}

Die Typisierung von --progress als <percentage> bedeutet, dass ein abweichender Nicht-Prozentwert auf 0% zurückfällt, anstatt width zu beschädigen.

Wann @property nicht verwendet werden sollte

Verzichten Sie auf die @property-Registrierung für benutzerdefinierte Eigenschaften, die niemals animieren, niemals externe Eingaben entgegennehmen und keine explizite Vererbungssteuerung benötigen. Die drei Deskriptoren erzeugen syntaktischen Mehraufwand ohne Laufzeitgewinn für rein statische Tokens. Eine Registrierung ist gerechtfertigt, wenn mindestens eine von drei Bedingungen zutrifft:

  1. Der Wert animiert oder wird in einem Übergang verwendet. Interpolation erfordert einen registrierten Typ.
  2. Der Wert empfängt externe Eingaben, die ungültig sein könnten. Ein Theme-Switcher, eine Benutzereingabe oder ein Build-Zeit-Token, das möglicherweise fehlerhaft ist, profitiert von der stillen Fallback-Garantie.
  3. Das Vererbungsverhalten muss explizit gesteuert werden. Wenn das Kaskadierungsverhalten einer Eigenschaft über verschachtelte Komponenten hinweg festgelegt werden muss.

Für eine statische Abstands-Skala, eine z-index-Hierarchie oder einen font-family-String, der einmalig gesetzt und weder animiert noch mit externen Eingaben gespeist wird, ist eine einfache benutzerdefinierte Eigenschaft in :root einfacher und erfüllt den Zweck. Das Hinzufügen von @property bringt dort drei zu pflegende Deskriptoren und kein Verhalten, das man anderweitig vermissen würde. Typisieren Sie die Eigenschaften, die sich bewegen oder Eingaben entgegennehmen; lassen Sie den Rest als Strings.

Typisierte benutzerdefinierte Eigenschaften machen den Browser zu einem Validator und einer Interpolations-Engine, aber der Preis dafür ist ein stilles Fehlerverhalten: Ungültige Eingaben fallen auf initial-value zurück, ohne eine Fehlerspur zu hinterlassen. Registrieren Sie die Eigenschaften, die animieren oder Laufzeiteingaben entgegennehmen, setzen Sie sinnvolle Anfangswerte, und betrachten Sie diesen Fallback als ein in der Produktion zu beobachtendes Verhalten – nicht als ein Sicherheitsnetz, das Probleme verbirgt.

Häufig gestellte Fragen

Die At-Regel @property ist eine Top-Level-At-Regel und wird eigenständig deklariert, nicht innerhalb von :root oder einem Selektor verschachtelt. Der @property-Block wird an beliebiger Stelle in Ihrem Stylesheet geschrieben, um den Typ global zu registrieren; anschließend wird der Wert der Eigenschaft innerhalb von :root oder einem beliebigen Selektor gesetzt, wie bei jeder anderen benutzerdefinierten Eigenschaft. Die Registrierung gilt dokumentweit, unabhängig davon, wo der Wert später zugewiesen wird. Das Platzieren von @property in einer globalen Token-Datei und das Zuweisen von Werten in :root ist daher das Standardmuster.

Beide registrieren eine typisierte benutzerdefinierte Eigenschaft mit identischem Laufzeitverhalten, aber @property ist deklaratives CSS, das ohne JavaScript wirksam wird, während CSS.registerProperty() imperativ zur Laufzeit ausgeführt wird. Verwenden Sie standardmäßig @property, da es bei Ihren Styles verbleibt und keine Skriptausführung benötigt. Greifen Sie auf CSS.registerProperty() nur zurück, wenn die Registrierung dynamisch erfolgen muss, etwa wenn syntax oder initialValue von Laufzeitbedingungen abhängen. Beachten Sie, dass CSS.registerProperty() camelCase initialValue verwendet, eine Eigenschaft nicht erneut registrieren kann und einen Fehler auslöst, wenn es zweimal für denselben Namen aufgerufen wird.

Nein. Eine untypisierte benutzerdefinierte Eigenschaft wird als undurchsichtiger String gespeichert, sodass der Browser zwei Strings ohne numerischen Mittelpunkt sieht und den Wert diskret wechselt, anstatt zu interpolieren. Die Registrierung der Eigenschaft mit @property gibt ihr einen Typ, den der Browser versteht, was Interpolation über Übergänge und Keyframes hinweg ermöglicht. Ein nicht registrierter Winkel springt beispielsweise vom Anfangs- zum Endwert, während dieselbe als <angle> registrierte Eigenschaft gleichmäßig tweent. Die Typregistrierung ist das, was Interpolation erst möglich macht.

Der Browser verwirft die ungültige Zuweisung und rendert das Element unter Verwendung des registrierten initial-value. Dies geschieht stillschweigend ohne Konsolenfehler und ohne visuellen Hinweis auf der gerenderten Seite, dass der Fallback ausgelöst wurde – ein Verhalten, das in der Spezifikation als „zum Zeitpunkt des berechneten Werts ungültig werden“ beschrieben wird. Die Seite bricht nicht, signalisiert aber auch nicht, dass etwas schiefgelaufen ist – was diese Regressionen für das Standard-Fehlermonitoring unsichtbar macht. DevTools zeigt den berechneten Fallback-Wert nur an, wenn Sie das Element direkt inspizieren.

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