wandres.dev
PROPIEDADES TIPADAS · @property y el registro

La gramática de syntax: tipos, combinadores y límites

El catálogo de componentes que acepta el descriptor syntax, los tres combinadores, para qué sirve registrar con la sintaxis universal, y por qué la gramática es deliberadamente pobre.

⏱ 16 min

El descriptor syntax no acepta la gramática de valores de CSS: acepta un subconjunto minúsculo con una docena larga de tipos, tres combinadores y ninguna forma de agrupar. Esa pobreza parece una limitación provisional y no lo es. Es la consecuencia directa de que un valor registrado no solo tiene que poder validarse, sino que tiene que poder interpolarse, y una gramática expresiva produce valores cuya forma varía.

🎯 Al terminar esta lección sabrás
  • Elegir el componente de sintaxis correcto para cada tipo de token.
  • Combinar componentes con listas y alternativas.
  • Saber por qué registrar con "*" sigue teniendo sentido.
  • Reconocer cuándo la gramática se queda corta y qué hacer entonces.

El catálogo de componentes

Estos son los nombres de componente que acepta syntax, cada uno entre comillas y con sus signos de menor y mayor:

Componente Acepta Uso típico
<length> 10px, 2rem, calc(...) espaciados, radios, tamaños
<number> 1, 0.75, -2 factores, multiplicadores
<percentage> 50% paradas de gradiente, proporciones
<length-percentage> ambos lo que acepte las dos cosas
<integer> 3, -1 contadores, número de columnas
<string> "hola" contenido textual
<color> red, oklch(...), #abc tokens de color
<angle> 45deg, 0.25turn rotaciones, gradientes cónicos
<time> 200ms, 0.3s duraciones y retardos
<resolution> 2dppx densidad de imagen
<image> url(...), linear-gradient(...) fondos
<url> url(...) recursos
<transform-function> rotate(45deg) una transformación
<transform-list> varias seguidas cadenas de transformación
<custom-ident> cualquier identificador nombres libres
* cualquier flujo de tokens sin tipo

Además de los componentes, puedes escribir identificadores literales directamente, y eso es lo que produce enumeraciones de verdad:

@property --alineacion {
  syntax: "inicio | centro | fin";
  inherits: true;
  initial-value: inicio;
}

Nótese la diferencia con <custom-ident>, que aceptaría cualquier identificador, incluido patata. Si lo que quieres es un conjunto cerrado de valores, enuméralos.

Los tres combinadores

+ construye una lista separada por espacios de uno o más elementos del componente.

@property --sangrado {
  syntax: "<length-percentage>+";
  inherits: false;
  initial-value: 0px;
}
/* Acepta: 1rem, o "1rem 2rem", o "10% 20% 10% 20%". */

# construye una lista separada por comas de uno o más elementos.

@property --paleta {
  syntax: "<color>#";
  inherits: true;
  initial-value: black;
}
/* Acepta: red, o "red, green, blue". */

| ofrece alternativas entre componentes o literales, y es el que más se usa en la práctica porque casi todo token útil admite una palabra clave además de un valor:

@property --ancho-columna {
  syntax: "auto | <length>";
  inherits: false;
  initial-value: auto;
}

Lo que no hay es agrupación. No existen paréntesis ni corchetes en la gramática de syntax, así que no puedes escribir algo como una alternativa entre dos listas ni hacer opcional una parte del valor. Los multiplicadores se aplican a un componente, y las alternativas se escriben en el nivel superior.

Dos componentes tienen prohibido llevar multiplicador. <transform-list> ya lleva uno dentro —es una forma abreviada exacta de <transform-function>+—, así que "<transform-list>+" es inválido. Y "*" no es un componente sino la sintaxis universal entera, así que no se puede multiplicar ni combinar con nada: "* | auto" no existe.

La sintaxis universal y para qué sirve registrarla

syntax: "*" acepta cualquier flujo de tokens, igual que una propiedad sin registrar. Con ella, initial-value es opcional, no hay validación, y el valor no se puede animar.

Visto así parece inútil. Tiene exactamente un motivo de peso: es la única forma de conseguir un flujo de tokens no heredable.

@property --interno {
  syntax: "*";
  inherits: false;
}

Con eso obtienes dos cosas que no se pueden tener de otra manera. La primera es encapsulación: un valor que solo usa el propio elemento deja de filtrarse a sus descendientes, y por tanto un componente anidado dentro de otro no hereda accidentalmente el parámetro interno de su contenedor. La segunda es la que importa en un perfil: cambiar esa propiedad invalida solo el elemento, no su subárbol, que es la palanca de rendimiento que ya viste al final del nivel anterior.

Elegir el tipo correcto

Tres criterios prácticos.

Registra el tipo más estrecho que admita todos los valores legítimos. <length> en vez de <length-percentage> si nunca vas a pasar porcentajes; "claro | oscuro" en vez de <custom-ident> si solo hay dos temas. Cuanto más estrecho, antes se detecta el error y mejor definida está la interpolación.

No uses <number> donde quieras una longitud. Es el error clásico de los sistemas de espaciado: registrar --espaciado como <number> para poder multiplicarlo, y acabar teniendo que escribir calc(var(--espaciado) * 1px) en cada uso. Registra la unidad de base como <length> y el multiplicador como <number>, cada uno en su propiedad.

Separa lo que quieras animar. Ésta es la regla que más cambia el diseño de un sistema de tokens y la desarrolla el siguiente apartado.

/* Un solo token compuesto: no se puede animar por partes. */
@property --sombra { syntax: "*"; inherits: false; }

/* Cuatro tokens simples: cada uno animable, y se componen en la declaracion. */
@property --sombra-x     { syntax: "<length>"; inherits: false; initial-value: 0px; }
@property --sombra-y     { syntax: "<length>"; inherits: false; initial-value: 2px; }
@property --sombra-difuminado { syntax: "<length>"; inherits: false; initial-value: 4px; }
@property --sombra-color { syntax: "<color>";  inherits: false; initial-value: rgb(0 0 0 / 0.2); }

.tarjeta {
  box-shadow: var(--sombra-x) var(--sombra-y) var(--sombra-difuminado) var(--sombra-color);
  transition: --sombra-y 200ms, --sombra-difuminado 200ms;
}
💡
Los literales también sirven para documentar

Una sintaxis como "fina | media | gruesa | <length>" no solo valida: aparece en las herramientas de desarrollo y en cualquier lectura del archivo de tokens como la lista de lo que ese parámetro admite. Es la documentación más barata que existe, porque el navegador la hace cumplir.

La gramática es pobre porque tiene que ser interpolable

Cuando descubres que no puedes agrupar ni hacer opcional una parte de la sintaxis, la reacción natural es pensar que la especificación se quedó a medias y que ya lo ampliarán. No es eso. La gramática de syntax no tiene que responder solo a la pregunta “¿este valor es aceptable?”, que es lo que hace la gramática de cualquier propiedad de CSS; tiene que responder también a “dado el valor A y el valor B, ¿cuál es el valor que está a un treinta por ciento del camino entre ellos?”. Y esa segunda pregunta solo tiene respuesta si ambos valores tienen la misma forma. Con una gramática que permita opcionalidad, 10px y 10px red serían ambos válidos para la misma propiedad, y no existe ninguna definición razonable de interpolar entre un valor con color y otro sin él. Con agrupación y alternativas anidadas, dos valores válidos pueden tener estructuras arbitrariamente distintas y el problema se vuelve intratable. Al limitar la gramática a un tipo, opcionalmente repetido, opcionalmente elegido entre alternativas de nivel superior, la especificación garantiza que cualquier par de valores válidos o bien tiene la misma forma —y entonces se interpola componente a componente— o bien no la tiene, y entonces la transición cae al comportamiento discreto, que también está definido. La consecuencia de diseño es la más útil de esta lección: cuando te encuentres queriendo una sintaxis que la gramática no puede expresar, no estás topando con una limitación de la herramienta, estás descubriendo que ese valor no es interpolable. Y la respuesta correcta no es buscar un rodeo sino descomponerlo en varias propiedades simples que sí lo sean, y componerlas en el punto de uso. Eso es exactamente lo que hace que una sombra o una transformación puedan animarse suavemente, y también, casualmente, lo que hace que un sistema de tokens sea más fácil de sobrescribir por partes.

⚔️ Tipa un sistema
  1. Registra un token de color, uno de longitud y uno de ángulo, y comprueba que cada uno rechaza lo que no es suyo.
  2. Escribe una sintaxis con alternativa entre una palabra clave y un tipo.
  3. Registra una propiedad con "*" y inherits: false y verifica que no llega a los descendientes.
  4. Descompón una sombra en cuatro propiedades registradas y anima solo el desenfoque.
  5. Busca en tu sistema de tokens un valor compuesto y decide si merece la pena partirlo.