@property y las trampas del valor
Por qué red y Red no son el mismo valor, cómo registrar la propiedad hace que la comparación sea semántica, y la fuga de herencia que hace que una consulta de estilo coincida donde no querías.
Una consulta de estilo compara valores computados, y el valor computado de una custom property sin registrar es una secuencia de tokens, no un concepto. Eso significa que --tono: Peligro y --tono: peligro son valores distintos, que --color: #ff0000 no coincide con style(--color: red), y que una propiedad heredada hace que la consulta se cumpla en cada nivel del subárbol hasta la hoja. Las tres cosas tienen solución y las tres pillan a todo el mundo la primera vez.
- Explicar cómo se comparan los valores de una custom property sin registrar.
- Registrar una propiedad con
@propertypara obtener comparación por valor computado. - Controlar el alcance de una consulta de estilo con
inherits: false. - Garantizar que
not style(...)se comporte de forma predecible con un valor inicial.
Comparación sin registrar: tokens, no significado
Una custom property que no has declarado con @property tiene el tipo universal. Su valor computado es la secuencia de tokens que escribiste, con las sustituciones de var() ya hechas y los espacios de los extremos recortados. La comparación de una consulta de estilo se hace sobre eso.
Las consecuencias son tres y son literales:
.a { --tono: Peligro; }
@container style(--tono: peligro) { /* NO coincide: distinta caja */ }
.b { --color: #ff0000; }
@container style(--color: red) { /* NO coincide: distintos tokens */ }
.c { --n: calc(2 + 3); }
@container style(--n: 5) { /* NO coincide: la expresion no se evalua */ }
El caso de calc() es el más contraintuitivo: en una propiedad sin registrar, calc(2 + 3) se guarda como los tokens de la expresión, no como el número 5. El navegador no la evalúa porque no sabe de qué tipo es ni si va a acabar en una longitud, un número o una cadena.
La regla práctica cuando no registras nada: usa identificadores literales, escritos siempre igual, en minúsculas y sin ambigüedad. Si tu valor es una palabra clave de tu propio vocabulario —compacta, oscura, error— la comparación por tokens es exactamente lo que quieres y no hay problema.
Registrar cambia la comparación
@property le da un tipo a la propiedad, y con un tipo el navegador puede computar el valor de verdad. La comparación pasa a ser por valor computado, que es lo que uno espera:
@property --acento {
syntax: "<color>";
inherits: true;
initial-value: oklch(0.7 0.15 250);
}
.tema { --acento: red; }
@container style(--acento: rgb(255 0 0)) {
/* SI coincide: los dos computan al mismo color */
.marca { color: var(--acento); }
}
Lo mismo con números y longitudes: registrada como <number>, --n: calc(2 + 3) computa a 5 y coincide con style(--n: 5).
Registrar aporta otras dos cosas que ya conoces de las custom properties tipadas y que aquí se vuelven especialmente útiles: un valor inicial garantizado, con lo que la propiedad nunca está indefinida, y validación, con lo que un valor mal escrito cae al inicial en lugar de propagar basura.
Una propiedad registrada como <color>, <length> o <number> se vuelve animable e interpolable, y eso puede activar transiciones que no esperabas si tienes un transition: all cerca. Además, el valor deja de ser un token opaco: si alguna vez dependías de guardar ahí una cadena arbitraria, registrarla con un tipo estricto la invalidará. Registra cuando necesites comparación semántica o animación, no por costumbre.
La fuga de herencia
Esta es la trampa que más tiempo cuesta. Las custom properties se heredan por defecto, y todo elemento es contenedor de consultas de estilo. Combina las dos cosas:
.panel { --superficie: oscura; }
@container style(--superficie: oscura) {
.caja { border-color: white; }
}
--superficie vale oscura en el panel, en sus hijos, en sus nietos y en toda la descendencia. Y como el contenedor de consultas de estilo de cualquier elemento es su padre, la condición se cumple para cualquier .caja a cualquier profundidad dentro del panel. Normalmente eso es justo lo que quieres: es lo que hace útil al patrón de superficies.
Deja de quererlo cuando hay una isla dentro. Una tarjeta clara sobre un panel oscuro sigue teniendo --superficie: oscura heredado en sus descendientes, así que sus cajas internas seguirán poniéndose el borde blanco. La consulta no distingue “el panel decidió” de “lo heredé sin querer”.
Hay dos formas de cortar la fuga.
Redeclarar en el punto de corte, que es lo directo y lo que suele bastar:
.tarjeta-clara { --superficie: clara; }
Registrar la propiedad como no heredable, que convierte el alcance en un solo nivel:
@property --modo {
syntax: "*";
inherits: false;
}
Con inherits: false, el valor solo existe en el elemento donde lo declaras; sus hijos reciben el valor inicial. Como el contenedor de consultas de un elemento es su padre, la consulta pasa a cumplirse únicamente para los hijos directos del elemento que declaró el modo. Es un mecanismo de alcance real, no un apaño, y es la forma correcta de modelar un modo que no debe atravesar componentes anidados.
Elige según lo que estés modelando: si es un ambiente que debe impregnar todo lo de dentro, herédalo; si es una instrucción para el nivel inmediato, no lo heredes.
Hacer predecible el caso negativo
not style(--tono: peligro) es verdadero cuando la propiedad tiene cualquier otro valor y también cuando no tiene ninguno. Eso puede dar lugar a que un bloque se aplique en sitios donde nunca pensaste en el tono.
La forma limpia de evitarlo es que la propiedad tenga siempre un valor definido, cosa que se consigue con un valor inicial:
@property --tono {
syntax: "neutro | aviso | peligro";
inherits: true;
initial-value: neutro;
}
Ese syntax con alternativas hace tres cosas a la vez: garantiza que el valor siempre es uno de los tres, invalida cualquier error tipográfico haciéndolo caer a neutro, y documenta el vocabulario del contrato en el propio CSS. Con eso, style(--tono: neutro) y not style(--tono: neutro) particionan el mundo de forma exhaustiva y sin sorpresas.
Lo que ocurre al registrar una custom property es exactamente el salto que da un lenguaje de programación al pasar de comparar cadenas a comparar valores tipados. Sin registro, CSS no sabe qué es --tono; solo puede decir si dos secuencias de tokens son idénticas, que es la única noción de igualdad disponible cuando no hay un tipo detrás. Con syntax: "<color>", el motor sabe que se trata de un color, lo normaliza a una representación canónica y la igualdad pasa a significar lo que un humano entiende por igualdad. Este es el mismo motivo por el que comparar fechas como cadenas es una mala idea, por el que comparar números en coma flotante con == da sorpresas, y por el que las bases de datos tienen tipos en vez de guardarlo todo como texto: la igualdad no es una operación universal, es una operación que depende del tipo. La costumbre profesional que sale de aquí es sencilla: en el momento en que un valor deje de ser un identificador opaco de tu propio vocabulario y pase a ser un color, un número o una longitud sobre la que quieras comparar o interpolar, regístralo. El coste son cinco líneas y lo que compras es dejar de razonar sobre cadenas.
- Declara
--tono: Peligrocon mayúscula y comprueba questyle(--tono: peligro)no coincide. - Registra la propiedad como
<color>y verifica queredyrgb(255 0 0)sí coinciden. - Monta un panel oscuro con una tarjeta clara dentro y observa la fuga de herencia.
- Corrígela primero redeclarando y después con
inherits: false, y compara los dos alcances. - Define un
syntaxcon tres alternativas, escribe un valor inválido y comprueba en el inspector a qué cae.