wandres.dev
QUÉ ES CSS DE VERDAD · Un lenguaje declarativo de restricciones

Cuando no funciona: el motor resolvió otra cosa

Las cuatro maneras distintas en que una declaración puede no surtir efecto, cómo se distinguen en menos de un minuto y el fallo silencioso más desconcertante del CSS moderno.

⏱ 18 min

“No funciona” no es un diagnóstico, es un síntoma, y bajo el mismo síntoma se esconden cuatro causas que no tienen nada que ver entre sí y que se arreglan en sitios distintos. La diferencia entre alguien que depura CSS en dos minutos y alguien que se pasa la tarde probando valores no está en saber más propiedades: está en tener el árbol de decisión y recorrerlo en orden en lugar de empezar por el final.

🎯 Al terminar esta lección sabrás
  • Clasificar cualquier fallo de estilo en una de las cuatro categorías de causa.
  • Distinguir en el inspector una declaración que perdió la cascada de una inválida.
  • Reconocer las propiedades que se aplican correctamente y aun así no hacen nada.
  • Explicar el fallo en tiempo de computación de las custom properties y por qué no cae al valor anterior.

Cuatro causas bajo un solo síntoma

Causa uno: la declaración nunca llegó. El selector no casa con el elemento, la regla se descartó por sintaxis inválida, el fichero no se cargó, o esa declaración perdió la cascada frente a otra. En el panel de estilos esto se ve: la declaración aparece tachada si perdió, aparece con un aviso si el valor es inválido, y directamente no aparece si el selector no casa.

Causa dos: llegó, pero no aplica a esa caja. La propiedad es válida, gana la cascada, está ahí, y aun así no tiene efecto porque su definición dice que solo aplica a cierto tipo de caja. Es la causa más traicionera porque no deja rastro visual: la declaración se ve perfecta en el inspector.

Causa tres: aplicó, pero otra restricción ganó. El valor participó en la ecuación y perdió, o hay un mínimo automático, o el bloque contenedor no es el que creías. Esto se ve en el panel de estilos calculados: el valor final no coincide con el declarado.

Causa cuatro: todo funcionó y tu expectativa era otra. El caso más frecuente en gente con experiencia, y el más difícil de admitir. Aquí el arreglo no está en el CSS sino en el modelo mental.

La utilidad del árbol está en el orden: cada causa se descarta con una comprobación distinta y barata, y saltarse la primera para ir a la tercera es lo que produce las tardes perdidas.

Comprobación Dónde se mira Si falla, la causa es
¿Aparece la declaración sin tachar? Panel de estilos del elemento Uno
¿Aplica esa propiedad a este tipo de caja? Documentación de la propiedad Dos
¿El valor calculado coincide con el declarado? Panel de estilos calculados Tres
¿El resultado es coherente con el algoritmo? Modelo de caja y árbol Cuatro

Propiedades que se aplican y no hacen absolutamente nada

La causa dos merece catálogo porque es la que más tiempo consume: la declaración está escrita, es válida, gana la cascada y no produce ningún efecto. Estas son las que se repiten.

Declaración No hace nada cuando Por qué
gap El contenedor no es flex, grid ni multicolumna Solo existe en esos contextos
z-index El elemento no está posicionado ni es hijo de flex o grid Solo participan los que crean orden de apilamiento
inline-size y block-size El elemento es display: inline no reemplazado Un elemento en línea no reemplazado se dimensiona por su contenido
vertical-align El elemento es una caja de bloque Es una propiedad del contexto de línea
position: sticky No hay ningún desplazamiento declarado Sin umbral no hay nada a lo que pegarse
align-items El contenedor no establece un contexto que lo use Es propiedad del contenedor, no del hijo
text-align Se espera que mueva una caja de bloque hija Alinea contenido en línea, no bloques
overflow Se aplica a un elemento en línea No genera caja con área de desplazamiento

Las dos primeras filas concentran la mayoría de los casos reales. Un z-index: 9999 que no sube nada casi siempre es un elemento sin position distinto de static, o un elemento que sí está posicionado pero cuyo ancestro forma su propio contexto de apilamiento y por tanto lo encierra: por muy alto que sea el número dentro de una caja, la caja entera se pinta donde le toca.

La forma de no caer nunca en la causa dos es leer la línea “se aplica a” de la documentación de la propiedad antes de escribirla. Es la línea que todo el mundo se salta y la que responde a la mitad de las preguntas.

El fallo que no cae al valor anterior: inválido en tiempo de computación

Hay un modo de fallo que rompe la intuición construida en toda esta guía, y que aparece en cuanto usas custom properties. Ya sabes que una declaración con un valor inválido se descarta y gana la declaración anterior de la misma propiedad; ese es el mecanismo que hace funcionar los planes alternativos escritos como dos líneas seguidas. Pues bien: con var() eso deja de ser cierto. Una declaración que contiene una referencia a variable es sintácticamente válida siempre, porque en el momento del análisis el motor no sabe qué habrá dentro. La sustitución ocurre después, al calcular valores, y si el resultado no es válido para esa propiedad ya es demasiado tarde para descartar la declaración: la cascada terminó hace rato y las alternativas se han olvidado. La especificación define entonces un comportamiento propio, llamado inválido en tiempo de computación, que consiste en tratar la propiedad como si hubieras escrito unset: hereda si es una propiedad heredable, y toma su valor inicial si no lo es. El efecto práctico es desconcertante la primera vez. Escribes color: red; color: var(--acento); con --acento: 16px, y no obtienes rojo: obtienes el color heredado del padre, que puede ser cualquier cosa. Un error tipográfico en una variable no degrada al valor anterior, salta a un valor completamente ajeno y sin ninguna relación con lo que hay alrededor. De ahí dos costumbres que ahorran horas: dar siempre un valor de reserva en el punto de uso con var(--acento, #d20f39), y registrar las variables críticas con @property y su syntax, porque una variable registrada con tipo rechaza los valores que no encajan en el momento de la declaración y ahí sí vuelve a funcionar el descarte normal.

El método: reducir hasta que no quede dónde esconderse

Cuando las cuatro comprobaciones no bastan —suele pasar cuando hay varios factores a la vez— el método que siempre funciona es la reducción. No es glamuroso y es imbatible.

Primero, aísla. Copia el elemento problemático y sus ancestros mínimos a un documento nuevo con el CSS estrictamente necesario. Si el problema desaparece, la causa está en algo que quitaste; vas devolviendo piezas hasta que reaparece. Si el problema persiste en veinte líneas, ya lo tienes acorralado.

Segundo, haz visible la geometría. Un contorno global es la herramienta más infravalorada de la depuración de layout, y outline es la propiedad correcta porque no ocupa espacio y por tanto no altera lo que estás midiendo:

/* Depuración temporal: no afecta al layout */
* { outline: 1px solid oklch(70% 0.15 20 / 0.6); }

Un border para lo mismo cambiaría los tamaños y te haría perseguir un problema que has creado tú.

Tercero, comprueba el bloque contenedor. Muchos porcentajes y todos los posicionados se resuelven contra un ancestro que a menudo no es el padre visual. Selecciona el elemento, mira su tamaño en el modelo de caja y compáralo con el ancestro que crees que manda. Cuando no cuadra, casi siempre hay en medio un ancestro con position, con transform, con filter o con contain, y cualquiera de ellos puede convertirse en el bloque contenedor de los descendientes posicionados.

Cuarto, desconfía de lo que hereda. Si el fallo aparece solo en algunos sitios del documento y no en otros con el mismo marcado, mira el árbol hacia arriba. La tipografía, el color, la dirección y las custom properties bajan solas, y un valor puesto tres niveles más arriba cambia el resultado sin aparecer en ninguna regla que apunte a tu elemento.

El hábito que evita la mayoría de estas sesiones

Todo lo anterior es reactivo. La medida preventiva que más reduce la frecuencia de estos episodios es una sola: escribir menos restricciones. Cada valor absoluto que fijas es una oportunidad de conflicto; cada auto que dejas es una vía de escape para el motor.

Y una segunda, más concreta: cuando algo funcione por casualidad, párate a averiguar por qué. Un arreglo que no entiendes es deuda garantizada, porque no sabes bajo qué condiciones dejará de funcionar, y volverá a aparecer transformado en otro sitio. En un lenguaje determinista como este, “no sé por qué funciona” siempre significa “todavía no he mirado”.