Registrar desde JavaScript y quién gana
Cómo se usa CSS.registerProperty, qué excepciones lanza, por qué el registro imperativo gana siempre al declarativo, y qué implica que registrar sea un efecto global e irreversible.
La misma familia de especificaciones que trajo @property trae su gemelo imperativo, CSS.registerProperty(). Hacen lo mismo, y sin embargo no son intercambiables: uno vive en la hoja de estilos y desaparece con ella, el otro vive en el documento y no se puede deshacer. Y cuando ambos registran el mismo nombre, la especificación no consulta el orden: el registro hecho desde JavaScript gana siempre.
- Registrar una propiedad desde código y conocer las excepciones que lanza.
- Explicar quién gana cuando el mismo nombre se registra por las dos vías.
- Escribir un registro seguro frente a duplicados y a falta de soporte.
- Elegir entre las dos formas según el caso, y nombrar sin colisionar.
CSS.registerProperty
CSS.registerProperty({
name: '--acento',
syntax: '<color>',
inherits: false,
initialValue: 'oklch(60% 0.18 260)',
});
Los cuatro campos se corresponden con los descriptores de @property, con un detalle que hace perder tardes: en JavaScript es initialValue en camel case, no initial-value. Escribirlo con guion no da error de sintaxis en JS: simplemente el campo se ignora y la llamada falla por falta de valor inicial.
Las excepciones están definidas con precisión y conviene distinguirlas:
InvalidModificationError cuando el nombre ya se registró desde JavaScript. Es el único caso que no es un SyntaxError, y es el que vas a encontrarte en cuanto un módulo se ejecute dos veces.
SyntaxError en todos los demás casos: nombre que no es un nombre válido de custom property, cadena de sintaxis que no se puede analizar, initialValue ausente cuando la sintaxis no es universal, initialValue que no encaja con la sintaxis, e initialValue que no es computacionalmente independiente —por ejemplo 3em, o cualquier cosa que contenga var().
Fíjate en que la comprobación de duplicado solo mira los registros hechos desde JavaScript. Registrar por código un nombre que ya tenía una regla @property no lanza nada: lo sobrescribe.
Quién gana
El orden de consulta es fijo y no depende de cuándo ocurra cada cosa:
- Si hay un registro hecho con
CSS.registerProperty()para ese nombre, ése es el registro. - Si no, y hay reglas
@propertyválidas en las hojas activas, gana la última en orden de documento. - Si no hay ninguna, la propiedad no está registrada.
De aquí salen tres consecuencias que merece la pena tener presentes.
El registro imperativo gana aunque se ejecute antes. No hay carrera que gestionar: da igual si el script corre antes o después de que se cargue la hoja de estilos. Si el script registra, manda el script.
Varias reglas @property con el mismo nombre son legales. Gana la última válida. Y “válida” es la palabra clave: una regla posterior a la que le falte inherits es inválida, se ignora entera, y por tanto no desbanca a la anterior.
Un registro declarativo puede desaparecer. Como se determina a partir de las hojas de estilo activas, quitar o desactivar la hoja que contiene el @property deja de registrar la propiedad. El registro imperativo no: se guarda en el documento y no hay forma de retirarlo. La especificación deja anotado que quizá se añada una en el futuro; hoy no existe.
Como la sintaxis registrada solo se aplica en tiempo de computación y no al analizar, una comprobación como @supports (--x: 1em) devuelve verdadero aunque --x esté registrada como <color>. @supports está preguntando si la declaración se analiza, y se analiza siempre. No hay forma desde CSS de preguntar si un valor concreto encajaría con una sintaxis registrada.
El patrón seguro
Tres cosas que cubrir: que la API exista, que el registro no esté duplicado, y que un fallo no tumbe el módulo.
export function registrar(definicion) {
if (!('registerProperty' in CSS)) return false;
try {
CSS.registerProperty(definicion);
return true;
} catch (error) {
// Ya estaba registrada: para nuestros efectos, exito.
if (error.name === 'InvalidModificationError') return true;
// Sintaxis o valor inicial mal escritos: eso si es un fallo nuestro.
throw error;
}
}
Tragar el InvalidModificationError y propagar el SyntaxError es la política correcta: el primero significa que otro ha hecho el trabajo, el segundo significa que tu definición está mal y quieres enterarte.
Cuándo compensa cada forma
Usa @property por defecto. Es declarativo, vive junto a los tokens que describe, se ve en las herramientas de desarrollo, funciona sin JavaScript, se aplica antes del primer pintado y desaparece limpiamente si quitas la hoja. Para el noventa y cinco por ciento de los casos —los tokens de un sistema de diseño— es la respuesta.
Usa CSS.registerProperty() cuando los nombres no se conocen de antemano. Una gráfica con un número variable de series, un tematizador que genera propiedades a partir de datos, un componente que registra una propiedad por instancia. Ahí el registro declarativo no se puede escribir porque no sabes cuántos ni cómo se llaman.
Y un aviso de tiempos: registrar tarde tiene efectos visibles. Los valores asignados antes del registro se trataron como no registrados, y al registrar cambian de valor calculado, lo que puede iniciar o interrumpir transiciones. Si registras desde código, hazlo lo antes posible, antes de que la página pinte.
Todo lo demás en CSS es reversible y acotable. Quitas una hoja de estilos y sus reglas dejan de aplicar. Cambias una clase y el elemento vuelve a su aspecto anterior. Incluso !important, que es lo más agresivo del lenguaje, se limita a ganar una comparación y no deja rastro cuando lo borras. El registro de una custom property desde JavaScript rompe esa propiedad en las tres direcciones a la vez: es global, porque no admite ámbito ni encapsulación y afecta al documento entero, incluidos los subárboles que no sabían nada de ti; es irreversible, porque no hay forma de retirarlo ni de sustituirlo, ya que reintentar lanza; y es prioritario, porque gana a cualquier @property sin importar el orden. Eso convierte al espacio de nombres de las custom properties registradas en un recurso compartido de facto entre todo el código que se ejecute en la página, con las mismas patologías que cualquier registro global mutable: dos librerías que registren --acento con sintaxis distintas son incompatibles y la incompatibilidad no se manifiesta como un error sino como valores que caen a un inicial ajeno, en componentes que no tienen nada que ver, sin traza que seguir. La disciplina que evita todo esto es aburrida y no negociable si escribes código que otros van a incluir: prefija con el nombre de tu librería todo lo que registres —--acme-acento, nunca --acento—, registra lo mínimo y hazlo desde @property siempre que puedas, para que al menos sea retirable quitando la hoja. Y si estás integrando código de terceros que registra nombres genéricos, sábelo: acaba de reclamar esa palabra para toda la página y no vas a poder discutírselo.
- Registra una propiedad desde JavaScript y comprueba que un segundo intento lanza
InvalidModificationError. - Escribe
initial-valueen lugar deinitialValuey observa qué excepción sale. - Registra desde JavaScript un nombre que ya tenía una regla
@propertyy verifica cuál manda. - Quita del documento la hoja que contiene un
@propertyy comprueba que la propiedad deja de estar registrada. - Audita los nombres que registra tu proyecto y prefija los que puedan colisionar.