wandres.dev
CUSTOM PROPERTIES · Variables que viven en la cascada

var() y el fallback: el flujo completo de resolución

Cuándo se sustituye una custom property, qué cuenta exactamente como fallback, el diagrama de resolución paso a paso, y por qué el fallback no es la red de seguridad que parece.

⏱ 17 min

var(--x, 1rem) se lee como “usa --x, y si algo va mal, usa 1rem”. Esa lectura es falsa y explica la mitad de los desconciertos con custom properties. El segundo argumento no es una red de seguridad frente a valores malos: solo cubre el caso de que la propiedad no exista. Si existe y contiene basura, el fallback no se usa, la declaración se rompe, y lo que ocurre entonces no se parece a nada de lo que hace el resto de CSS.

🎯 Al terminar esta lección sabrás
  • Situar en qué momento del cálculo de estilos ocurre la sustitución.
  • Delimitar exactamente qué parte de var() es el fallback.
  • Recorrer el diagrama completo de resolución y saber en qué rama cae cada caso.
  • Encadenar custom properties sin crear ciclos ni pagar de más.

La función y su sustitución

var() toma el nombre de una custom property y opcionalmente un valor de reserva. La sustitución no ocurre al analizar la hoja de estilos: ocurre en el valor calculado, es decir, después de que la cascada haya decidido qué declaraciones ganan y después de que la herencia haya llenado los huecos.

Ese momento importa. Cuando el motor sustituye, ya sabe qué valor tiene --x en este elemento concreto, con lo que haya heredado y lo que haya sobrescrito. Por eso la misma regla produce resultados distintos en sitios distintos, y por eso no se puede resolver antes.

.tarjeta {
  --relleno: 1rem;
  padding: var(--relleno);
  gap: calc(var(--relleno) / 2);
}

var() puede aparecer en cualquier punto del valor de una propiedad: sola, dentro de calc(), dentro de otra función, varias veces en una abreviada, y también dentro del valor de otra custom property.

El fallback: todo lo que va tras la primera coma

La regla es literal y sorprende: el fallback es todo lo que sigue a la primera coma, comas incluidas.

/* El fallback son DOS sombras, no la primera. */
box-shadow: var(--sombra, 0 0 0 1px red, 0 0 0 4px blue);

/* Fallbacks encadenados: se prueban en orden. */
color: var(--acento, var(--marca, var(--texto, black)));

/* Fallback vacio: valido, y significa sustituir por nada. */
padding-inline-end: var(--extra,);

El fallback vacío es útil y poco conocido. var(--x,) con --x sin definir sustituye por la cadena vacía, lo que en muchos contextos deja la declaración inválida, pero en otros —dentro de una lista, dentro de una abreviada— es exactamente lo que quieres.

Y lo importante: el fallback solo se usa cuando la custom property no tiene valor. Formalmente, cuando su valor es el llamado valor garantizadamente inválido, que es lo que tiene una propiedad no declarada, o una a la que has asignado initial a propósito.

.a { color: var(--no-existe, green); }   /* verde: la propiedad no existe */
.b { --tono: initial; color: var(--tono, green); }  /* verde: initial la vacia */
.c { --tono: patata; color: var(--tono, green); }   /* NO es verde. Ver mas abajo. */

El tercer caso es el que rompe la intuición. --tono tiene valor: vale patata. La sustitución se hace, el resultado es color: patata, y eso no es un color. El fallback no interviene porque la propiedad existía. Lo que ocurre entonces es el asunto de la lección siguiente.

El flujo completo de resolución

flowchart TB
A[El motor encuentra var en un valor] --> B{La custom property tiene valor en este elemento}
B -- No o vale initial --> C{Hay fallback}
C -- Si --> D[Se sustituye el fallback]
C -- No --> X[Declaracion invalida en tiempo de computacion]
B -- Si --> E[Se sustituye el flujo de tokens tal cual]
D --> F{El resultado encaja con la gramatica de la propiedad}
E --> F
F -- Si --> G[Valor aplicado]
F -- No --> X
X --> Y[La propiedad toma unset]
style G fill:#a6e3a1,color:#11111b
style X fill:#f38ba8,color:#11111b
style Y fill:#f9e2af,color:#11111b
style B fill:#89b4fa,color:#11111b
style F fill:#89b4fa,color:#11111b

Los dos rombos azules son preguntas distintas y ahí está toda la confusión. El primero pregunta si existe; el fallback solo responde a ese. El segundo pregunta si lo sustituido sirve; el fallback no lo cubre, y cuando la respuesta es no, se cae por la rama roja hacia un comportamiento que no es el descarte silencioso al que estás acostumbrado.

Merece la pena leer el diagrama una segunda vez fijándose en que la rama del fallback vuelve a entrar en la comprobación de gramática. Es decir: un fallback también puede ser inválido. var(--no-existe, patata) no es más seguro que var(--tono) con --tono: patata.

Cadenas, ciclos y el coste

Una custom property puede definirse a partir de otras, y esa es la base de los sistemas de tokens en capas:

:root {
  --marca-base: oklch(60% 0.18 260);
  --acento: var(--marca-base);
  --boton-fondo: var(--acento);
}

.boton { background: var(--boton-fondo); }

Tres saltos hasta el color final. El motor los resuelve al calcular el valor de --boton-fondo en cada elemento, y ahí hay un coste que conviene conocer: cada nivel de indirección es trabajo por elemento, no trabajo hecho una vez. En una página con miles de nodos, una cadena de seis niveles se resuelve miles de veces. No es motivo para renunciar a los tokens en capas —tres niveles es lo habitual y es perfectamente razonable— pero sí para no construir jerarquías de diez.

Los ciclos están prohibidos y su tratamiento es tajante:

:root {
  --a: var(--b);
  --b: var(--a);
}

El motor detecta el ciclo y todas las propiedades implicadas pasan a valer el valor garantizadamente inválido. No es que una gane y otra pierda: se invalidan las dos. Y como quedan sin valor, cualquier var(--a, reserva) que las use cogerá su fallback, porque el ciclo las ha dejado exactamente en el estado de “no existe”.

Ese detalle es sutil y a veces útil: el ciclo produce ausencia, no basura. La basura es peor.

⚠️
Un var() en una abreviada rompe más de lo que parece

Cuando escribes font: var(--tipo) y --tipo no encaja con la gramática de font, no pierdes una propiedad: pierdes todas las longhand que la abreviada controla —font-family, font-size, font-weight, line-height y compañía—, porque la declaración inválida afecta a la abreviada entera. Es una de las razones para preferir las longhand cuando el valor viene de una custom property.

El fallback es un valor por defecto, no una guarda

La palabra “fallback” invita a pensar en un try/catch, y de ahí sale un patrón de código que se ve por todas partes y que no protege de nada: escribir var(--x, 1rem) en cada uso con la sensación de haber blindado la regla. No la has blindado. Has declarado un valor por defecto para el caso de ausencia, que es un caso benigno y fácil de diagnosticar —la propiedad no está definida, típicamente porque el componente se usó fuera del contexto que la define— y no has hecho absolutamente nada frente al caso peligroso, que es que la propiedad exista con un valor que no sirve ahí. Y el caso peligroso es el que ocurre en producción: alguien escribe --espaciado: 8 sin unidad, alguien pasa un color en una propiedad de longitud desde JavaScript, alguien define un token con una errata. En todos esos, tu fallback mira desde la barrera. Interiorizar esto tiene dos consecuencias prácticas. La primera es que la verificación de que un valor sirve no puede estar en el punto de uso, porque var() no ofrece ningún mecanismo para hacerla: tiene que estar en la definición, y para eso existe el registro con @property, que es lo único en todo CSS capaz de decir “esta propiedad es una longitud y si le metes otra cosa se ignora”. La segunda es que, mientras no registres, el patrón defensivo que sí funciona no es el fallback sino no exponer nunca un parámetro sin declarar su valor por defecto en el propio componente: si .tarjeta declara --radio: 0.5rem en su propia regla, la ausencia deja de ser posible, el fallback deja de hacer falta, y el único fallo que queda es el que el fallback nunca iba a cubrir. Escribe el valor por defecto donde se define, no donde se consume.

⚔️ Sigue el flujo
  1. Explica por qué var(--x, green) con --x: patata no da verde.
  2. Escribe un var() cuyo fallback contenga dos sombras y comprueba que se aplican ambas.
  3. Encadena tres fallbacks y verifica en el inspector cuál se usa en cada caso.
  4. Crea un ciclo entre dos custom properties y observa que ambas quedan sin valor.
  5. Sustituye todos los fallbacks de un componente por valores por defecto declarados en su propia regla.