Trabajando con Variables CSS Tipadas Usando @property
La regla at-rule @property de CSS asigna un tipo a una propiedad personalizada. Una vez registrada, el navegador valida cada asignación, interpola entre valores durante las animaciones y recurre a un valor inicial definido cuando la entrada es inválida. @property corrige las deficiencias de validación e interpolación de las variables CSS, pero cambia el modo de fallo de una manera que la mayoría de los desarrolladores no espera: reemplaza un valor visiblemente roto con un fallback silencioso que no genera ningún error.
Este artículo cubre los tres descriptores y cuáles son obligatorios, el registro de tipos soportados con ejemplos concretos, el comportamiento del fallback silencioso y lo que los usuarios ven cuando se activa, la animación de propiedades tipadas más allá de la típica demostración de rotación, el equivalente en JavaScript con CSS.registerProperty(), el soporte actual en navegadores y un criterio de decisión para determinar cuándo el registro no justifica la complejidad adicional.
Conclusiones Clave
- Una propiedad personalizada CSS sin tipo es una cadena de texto;
@propertyle asigna un tipo que el navegador valida en cada punto de asignación. - Los descriptores
syntaxeinheritsson siempre obligatorios, einitial-valuees obligatorio siempre quesyntaxno sea"*"— según la especificación CSS Properties and Values API Level 1. - Cuando una propiedad registrada recibe un valor que no coincide con su
syntaxdeclarada, el navegador revierte silenciosamente ainitial-valuesin generar ningún error en la consola ni ningún indicador visual de que el fallback se activó. - Las propiedades tipadas interpolan durante las transiciones y animaciones; las propiedades personalizadas sin tipo no lo hacen, porque el navegador las ve como dos cadenas opacas sin un punto medio numérico.
@propertyestá disponible como Baseline Newly Available desde el 9 de julio de 2024 — las advertencias de “experimental” en referencias anteriores a 2024 están obsoletas.
El problema: las propiedades personalizadas sin tipo son solo cadenas de texto
Una propiedad personalizada CSS estándar almacena una cadena sin procesar hasta que se sustituye en una propiedad real. El navegador no sabe si --accent debe ser un color, una longitud o una palabra clave. No realiza ninguna validación en el punto de declaración, no puede interpolar entre dos valores durante una animación y no proporciona ningún tipo de retroalimentación cuando un valor es estructuralmente incorrecto para el uso que se le pretende dar.
Esta tercera deficiencia es la más relevante en la práctica. Consideremos una propiedad sin tipo usada en un text-shadow:
.card {
--accent: red;
text-shadow: 4px 2px 5px var(--accent);
}
/* en otro lugar, por error */
.card {
--accent: 20px;
}
La declaración text-shadow se vuelve inválida en el momento de la sustitución y la sombra desaparece. No hay ninguna advertencia en el punto donde se asignó 20px a --accent, porque en ese momento sigue siendo solo una cadena de texto. El navegador no tiene noción de que esta propiedad debía ser un color. La guía de propiedades personalizadas de MDN describe este modelo de sustitución: el valor de una propiedad personalizada se resuelve únicamente cuando se referencia mediante var().
@property, definida en la especificación CSS Properties and Values API Level 1, añade un tipo a la propiedad en sí misma. Una vez registrada, el navegador sabe que --accent es un <color> y lo hace cumplir en cada asignación, no solo en la sustitución.
Sintaxis: los tres descriptores y cuáles son obligatorios
Discover how at OpenReplay.com.
La regla at-rule @property acepta tres descriptores: syntax, inherits e initial-value. Los descriptores syntax e inherits son siempre obligatorios; initial-value es obligatorio siempre que syntax no sea "*" — omitirlo en un registro tipado hace que todo el bloque @property sea inválido y sea ignorado.
@property --accent {
syntax: "<color>";
inherits: false;
initial-value: #586de7;
}
syntax— una cadena que describe el tipo aceptado, extraída de un conjunto fijo de nombres soportados definidos por la especificación (cubiertos en la siguiente sección).inherits— un booleano (trueofalse) que controla si la propiedad hereda hacia abajo en el árbol del DOM. Este es el mismo comportamiento de herencia que tiene cualquier propiedad CSS; definirlo explícitamente es lo que hace que las propiedades tipadas sean predecibles en componentes anidados.initial-value— el valor utilizado cuando no se aplica ningún otro valor válido, y el valor al que la propiedad recurre ante una entrada inválida.
La especificación CSS Properties and Values API Level 1, §3.1 define el requisito con precisión: una regla @property es inválida si falta syntax o inherits, y también es inválida si falta initial-value a menos que syntax sea el universal "*". Varios tutoriales existentes describen initial-value como incondicionalmente obligatorio; la especificación lo vincula al valor de syntax, e inherits no tiene ningún efecto sobre esta condición. Una regla @property inválida es descartada — el registro simplemente no ocurre y la propiedad revierte a su comportamiento sin tipo.
El registro de tipos de CSS @property
El descriptor syntax acepta los nombres de componentes de sintaxis soportados definidos por la especificación CSS Properties and Values API Level 1, §2 — incluyendo <color>, <length>, <percentage>, <integer>, <angle>, <image> y <custom-ident> — además de multiplicadores (+ para listas separadas por espacios, # para listas separadas por comas) y sintaxis de unión (<color> | <length>) para propiedades que legítimamente aceptan múltiples tipos. Esta es una lista fija de nombres soportados, no una puerta abierta a cualquier palabra clave de tipo CSS.
Valor de syntax | Acepta | Rechaza | Ejemplo de initial-value |
|---|---|---|---|
"<color>" | cualquier color válido (#f00, rebeccapurple, oklch(...)) | longitudes, palabras clave como darkpink | #586de7 |
"<length>" | px, rem, em, vw, etc. | números sin unidad, porcentajes | 20px |
"<percentage>" | 50% | longitudes, números sin unidad | 100% |
"<integer>" | números enteros (12) | 1.5, longitudes | 12 |
"<angle>" | deg, rad, turn, grad | números sin unidad | 0deg |
"<image>" | url(...), gradientes | colores, longitudes | url(bg.png) |
"<custom-ident>" | identificadores definidos por el autor | números, cadenas entre comillas | none |
"*" | cualquier valor (paso sin tipo) | nada — acepta todo | opcional |
Tres extensiones de gramática amplían lo que una sola propiedad puede aceptar:
/* "+" — una lista de longitudes separadas por espacios */
@property --insets {
syntax: "<length>+";
inherits: false;
initial-value: 0px;
}
/* "#" — una lista de colores separados por comas */
@property --stops {
syntax: "<color>#";
inherits: false;
initial-value: black;
}
/* "|" — una unión: acepta una longitud o la palabra clave "auto" */
@property --gap {
syntax: "<length> | auto";
inherits: false;
initial-value: auto;
}
El multiplicador + indica una lista separada por espacios; # indica una lista separada por comas, según la sección de cadenas de sintaxis soportadas de la especificación. La unión | permite que una propiedad acepte más de un tipo — útil para propiedades que genuinamente aceptan, por ejemplo, una longitud o una palabra clave. La sintaxis universal "*" deshabilita completamente la verificación de tipos; es el único caso en que initial-value es opcional, ya que no hay ningún tipo al que recurrir por defecto. Para definiciones por tipo, la referencia de tipos de valores CSS de MDN y el índice de tipos del CSSWG listan cada nombre de componente.
Validación: el fallback silencioso que no se espera
Cuando una propiedad registrada recibe un valor que no coincide con su syntax declarada, el navegador descarta la asignación y renderiza el elemento usando initial-value. En los navegadores actuales, este fallback no produce ningún error en la consola ni ninguna indicación visual en la página renderizada de que el fallback se activó — la página no se rompe, pero tampoco indica que algo salió mal.
@property --hue {
syntax: "<angle>";
inherits: false;
initial-value: 90deg;
}
.card {
--hue: 220deg; /* ✅ válido, se usa */
--hue: #f00; /* ❌ tipo inválido, ignorado — revierte a 90deg */
background: oklch(70% 0.15 var(--hue));
}
El background siempre se resuelve a un color válido. Después de la asignación inválida, --hue no se convierte en #f00 ni queda vacío — el valor inválido se descarta y la propiedad se resuelve a su initial-value registrado de 90deg. La página de MDN sobre @property documenta esto como que la propiedad se vuelve “inválida en el momento del valor calculado”, lo que se resuelve al valor inicial registrado.
Esto es genuinamente mejor que un layout visiblemente roto. Pero también introduce una nueva clase de fallo. Las propiedades personalizadas sin tipo fallan de forma visible — la declaración dependiente se rompe y se puede ver. Las propiedades tipadas fallan silenciosamente: un selector de temas en JavaScript escribe un color malformado, un valor proporcionado por el usuario no se puede parsear, un token de diseño tiene la unidad incorrecta, y el componente se renderiza en su estado predeterminado sin dejar ningún rastro de error. Las DevTools muestran el valor de fallback calculado si se inspecciona el elemento, pero nada aparece en la consola en tiempo de ejecución.
Esta es la clase de bug que la reproducción de sesiones está diseñada para detectar. Cuando una propiedad personalizada tipada recibe una entrada inválida en producción — proveniente de la entrada del usuario, un token mal configurado o un cambio de tema en tiempo de ejecución — el navegador recurre silenciosamente a initial-value, no se dispara ningún error de JavaScript y el monitoreo de errores estándar no genera ninguna señal. La única evidencia post-despliegue es visual: un componente renderizado con el color o tamaño incorrecto. Las reproducciones de sesiones de estas implementaciones frecuentemente revelan el estado incorrecto directamente, capturando el DOM renderizado en el momento en que se asignó el valor erróneo, donde una herramienta que solo monitorea la consola no detecta nada.
Animación: interpolando propiedades tipadas
Las propiedades personalizadas registradas interpolan durante las animaciones; las no registradas, en cambio, animan de forma discreta. Esta es la consecuencia más útil del tipado. Dado que el navegador entiende que --hue es un <angle> y no una cadena de texto, puede interpolar entre 0deg y 360deg a lo largo de una transición — algo imposible con una propiedad personalizada sin tipo, donde el navegador ve dos cadenas opacas sin un punto medio numérico. La especificación CSS Transitions define la interpolación como una operación sobre valores tipados; una propiedad personalizada no registrada no tiene tipo, por lo que el navegador la intercambia de forma discreta en lugar de hacer una transición suave.
Todos los demás tutoriales demuestran esto con transform: rotate(). Aquí se presenta un caso más ilustrativo — animar el canal de matiz de un color oklch(), que muestra que el tipado permite interpolar un valor dentro de una función, no solo una propiedad independiente:
@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;
}
}
El swatch recorre suavemente toda la rueda de matices porque el navegador hace una transición de --hue desde 0deg hasta 360deg y recalcula oklch(65% 0.2 var(--hue)) en cada fotograma. La especificación CSS Color Level 4 define el argumento de matiz de oklch() como aceptando un <angle>, que es exactamente lo que registramos. Si se elimina el bloque @property, la animación se rompe: --hue se convierte en una cadena sin tipo, el navegador no puede interpolarlo y el swatch salta del inicio al final en lugar de ciclar suavemente. Esa comparación antes/después es la demostración más clara de por qué el registro importa para el movimiento.
El equivalente en JavaScript: CSS.registerProperty()
CSS.registerProperty() es el equivalente imperativo de la regla at-rule @property. Registra una propiedad personalizada tipada en tiempo de ejecución desde JavaScript, recibiendo un objeto con name, syntax, inherits y un initialValue opcional:
window.CSS.registerProperty({
name: "--hue",
syntax: "<angle>",
inherits: false,
initialValue: "0deg",
});
Nótese el initialValue en camelCase en la API de JS frente al descriptor con guion initial-value en CSS. La referencia de MDN para CSS.registerProperty() documenta los nombres de los parámetros y el comportamiento. Ambas rutas de registro son equivalentes en efecto; una propiedad registrada por cualquiera de los dos métodos es tipada y validada de forma idéntica.
Se recomienda usar la regla at-rule por defecto — convive con el resto de los estilos, es declarativa y no requiere ejecución de JavaScript para surtir efecto. Se debe usar CSS.registerProperty() cuando el registro deba ser dinámico: una propiedad cuyo syntax o initialValue depende de condiciones en tiempo de ejecución, o una librería que registra propiedades de forma programática como parte de su inicialización. Hay que tener en cuenta que una propiedad registrada con CSS.registerProperty() no puede volver a registrarse, por lo que se debe proteger contra su ejecución duplicada.
Soporte en navegadores
A partir del 9 de julio de 2024, @property está disponible como Baseline Newly Available — con soporte en las versiones actuales de Chrome, Firefox y Safari — lo que hace obsoletas las advertencias de “experimental” en referencias más antiguas. Firefox añadió soporte en la versión 128, lanzada en julio de 2024, completando así el soporte entre navegadores; Safari lo incorporó en la versión 16.4; Chrome lo soporta desde la versión 85. El anuncio de Baseline en web.dev confirma la fecha y el estado. Para datos exactos de versiones, consultar caniuse. Los tutoriales publicados antes de mediados de 2024 describen el soporte como “experimental” o “próximo”; esas afirmaciones ya no son válidas.
Patrones en el mundo real
Los usos de mayor valor de @property comparten una característica: la propiedad o bien anima, o bien recibe entrada externa, o bien necesita un control explícito de la herencia.
Definición global, consumo con alcance local
Los bloques @property se definen una vez en una capa global de tokens; los componentes que los consumen referencian la variable con var() como de costumbre. Todos los tutoriales de referencia declaran @property directamente encima de la regla que lo utiliza, lo cual resulta engañoso para el trabajo con sistemas de diseño — el patrón realista separa el registro del consumo:
/* tokens.css — cargado una vez en la raíz del documento */
@property --brand-hue {
syntax: "<angle>";
inherits: true;
initial-value: 250deg;
}
@property --surface {
syntax: "<color>";
inherits: true;
initial-value: #1a1a1a;
}
/* card.css — el componente nunca referencia el registro */
.card {
background: var(--surface);
border-color: oklch(60% 0.1 var(--brand-hue));
}
Cualquier asignación a --surface — desde un selector de temas, una media query o la entrada del usuario — es validada. Establecer inherits: true permite que los tokens se propaguen en cascada a los elementos descendientes.
Tematización de colores con superficies adaptativas
Los tokens de color tipados permiten que un único --brand-hue gestione una paleta de superficies a través de oklch(); un valor de matiz malformado recurre al valor inicial registrado en lugar de romper la paleta:
html:has(#dark:checked) {
--surface: oklch(20% 0.1 var(--brand-hue));
}
html:has(#light:checked) {
--surface: oklch(95% 0.04 var(--brand-hue));
}
Indicadores de progreso basados en scroll
Un <percentage> o <length> tipado se lee claramente como un valor de progreso e interpola suavemente cuando es controlado por una animación o actualizado desde 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}%`);
}
Tipar --progress como <percentage> significa que un valor que no sea un porcentaje revertirá a 0% en lugar de corromper width.
Cuándo no usar @property
Se puede omitir el registro con @property para las propiedades personalizadas que nunca animan, nunca reciben entrada externa y no necesitan un control explícito de la herencia. Los tres descriptores añaden sobrecarga sintáctica sin ningún beneficio en tiempo de ejecución para tokens puramente estáticos. El registro está justificado cuando se cumple al menos una de estas tres condiciones:
- El valor anima o hace transición. La interpolación requiere un tipo registrado.
- El valor recibe entrada externa que podría ser inválida. Un selector de temas, la entrada del usuario o un token de tiempo de compilación que podría estar malformado se beneficia de la garantía del fallback silencioso.
- El comportamiento de herencia necesita un control explícito. Cuando se necesita fijar el comportamiento de cascada de una propiedad en componentes anidados.
Para una escala de espaciado estática, una jerarquía de z-index o una cadena font-family que se establece una vez y nunca se anima ni recibe entrada externa, una propiedad personalizada simple en :root es más sencilla y cumple su función. Añadir @property en ese caso implica mantener tres descriptores adicionales sin obtener ningún comportamiento que de otro modo no se tendría. Se deben tipar las propiedades que se mueven o reciben entrada; el resto puede dejarse como cadenas de texto.
Las propiedades personalizadas tipadas convierten al navegador en un validador y un motor de interpolación, pero el coste es un modo de fallo silencioso: la entrada inválida revierte a initial-value sin dejar ningún rastro de error. Se recomienda registrar las propiedades que animan o reciben entrada en tiempo de ejecución, establecer valores iniciales sensatos y tratar ese fallback como un comportamiento a vigilar en producción, en lugar de una red de seguridad que oculta problemas.
Preguntas Frecuentes
La regla at-rule @property es una regla de nivel superior y se declara de forma independiente, sin estar anidada dentro de :root ni de ningún selector. El bloque @property se escribe en cualquier parte de la hoja de estilos para registrar el tipo de forma global; luego se establece el valor de la propiedad dentro de :root o de cualquier selector, como se haría con cualquier propiedad personalizada. El registro se aplica a todo el documento independientemente de dónde se asigne el valor posteriormente, por lo que el patrón estándar es colocar @property en un archivo global de tokens y asignar los valores en :root.
Ambos registran una propiedad personalizada tipada con un comportamiento en tiempo de ejecución idéntico, pero @property es CSS declarativo que surte efecto sin JavaScript, mientras que CSS.registerProperty() se ejecuta de forma imperativa en tiempo de ejecución. Se recomienda usar @property por defecto, ya que convive con los estilos y no requiere ejecución de scripts. Se debe recurrir a CSS.registerProperty() solo cuando el registro deba ser dinámico, por ejemplo, cuando syntax o initialValue dependen de condiciones en tiempo de ejecución. Hay que tener en cuenta que CSS.registerProperty() usa initialValue en camelCase, no puede volver a registrar una propiedad y lanza un error si se llama dos veces con el mismo nombre.
No. Una propiedad personalizada sin tipo se almacena como una cadena opaca, por lo que el navegador ve dos cadenas sin un punto medio numérico e intercambia el valor de forma discreta en lugar de interpolarlo. Registrar la propiedad con @property le asigna un tipo que el navegador comprende, lo que habilita la interpolación en transiciones y keyframes. Por ejemplo, un ángulo no registrado salta del inicio al final, mientras que la misma propiedad registrada como <angle> hace una transición suave. El registro del tipo es lo que hace posible la interpolación.
El navegador descarta la asignación inválida y renderiza el elemento usando el initial-value registrado. Esto ocurre silenciosamente, sin ningún error en la consola y sin ninguna indicación visual en la página renderizada de que el fallback se activó — comportamiento descrito en la especificación como volverse inválido en el momento del valor calculado. La página no se rompe, pero tampoco señala que algo salió mal, lo que hace que estas regresiones sean invisibles para el monitoreo de errores estándar. Las DevTools muestran el valor de fallback calculado solo si se inspecciona el elemento directamente.
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..