wandres.dev
SOPORTE Y DEGRADACIÓN · @supports y las capas de fallback

Detectar una declaración con @supports

Cómo evalúa el motor una condición de soporte, por qué comprueba análisis sintáctico y no comportamiento, y qué añade @supports sobre el descarte de declaraciones que CSS ya hacía solo.

⏱ 16 min

CSS lleva degradando desde 1996 sin que nadie escriba una condición: el analizador tira al suelo cualquier declaración que no entiende y se queda con la anterior. Ese mecanismo es tan bueno que la mayoría de las veces no necesitas nada más. @supports existe porque falla en dos casos muy concretos, y entender cuáles son es lo que separa usarlo bien de rodearlo todo de condiciones inútiles.

🎯 Al terminar esta lección sabrás
  • Explicar qué hace el descarte de declaraciones inválidas y dónde deja de servir.
  • Escribir condiciones de soporte de propiedad y de valor que evalúen lo que crees.
  • Predecir el resultado de una condición con sintaxis desconocida dentro de los paréntesis.
  • Usar CSS.supports() con sus dos firmas desde JavaScript.

El fallback que ya tenías

Cuando el analizador de CSS encuentra una declaración cuyo valor no sabe interpretar, la descarta entera y sigue. No aborta la regla, no ignora el bloque: elimina esa línea como si nunca se hubiera escrito. De ahí sale el patrón más viejo del lenguaje.

.panel {
  width: 40rem;
  width: min(40rem, 100%);
}

Un motor sin min() se queda con la primera línea. Uno moderno aplica la segunda porque llega después y gana por orden de aparición. No hay condición, no hay detección y no hay coste: el propio orden de la cascada hace de estructura de control.

Ese mecanismo tiene exactamente dos agujeros.

El primero: no puede descartar un grupo. Si una mejora consiste en cinco declaraciones que solo tienen sentido juntas, el analizador va a quedarse con las cuatro que entiende y tirar la quinta, y eso puede ser peor que no aplicar ninguna. Un position: sticky sin su top, o un display: grid con gap pero sin las pistas, dejan la interfaz en un estado intermedio que nadie ha diseñado.

El segundo: no puede condicionar una propiedad distinta. El descarte solo actúa sobre la declaración que no entiende. Si quieres decir “cuando exista :has(), quítale el padding a este otro elemento”, no hay forma de expresarlo con orden de declaraciones, porque el padding es válido en todos los navegadores y ninguno lo va a descartar.

@supports cubre esos dos agujeros y nada más. Cualquier uso que no caiga en uno de ellos probablemente sea ruido.

Cómo evalúa el motor la condición

La forma canónica lleva una declaración completa entre paréntesis:

@supports (display: grid) {
  .rejilla { display: grid; }
}

El navegador coge lo que hay dentro de los paréntesis, lo analiza como si fuera una declaración dentro de un bloque de estilo vacío y responde a una sola pregunta: ¿la conservaría o la descartaría? Si la conservaría, la condición es verdadera. No aplica nada, no mira ningún elemento, no consulta el layout. Es una pregunta sobre el analizador.

De ahí salen tres consecuencias que conviene tener grabadas.

La propiedad sola no vale. @supports (rotate) no pregunta si existe la propiedad rotate: no es una declaración, así que cae en la producción de general enclosed de la gramática, que la especificación define como “sintaxis futura desconocida” y obliga a evaluar como falso. No es un error de análisis y no rompe nada; simplemente siempre da negativo. Si escribes eso pensando que detectas la propiedad, obtienes el camino de fallback en todos los navegadores del mundo, incluidos los que la soportan.

Los paréntesis son obligatorios. @supports display: grid no es válido. Y cuando combinas condiciones, cada una lleva los suyos.

El valor importa tanto como la propiedad. Esta es la parte útil: como la pregunta es sobre la declaración completa, puedes detectar valores concretos de propiedades que existen desde siempre.

@supports (height: 100dvh)                      { }
@supports (width: 1lh)                          { }
@supports (color: color(display-p3 1 0 0))      { }
@supports (background: linear-gradient(in oklch, red, blue)) { }
@supports (animation-timeline: scroll())        { }

Ese es el mecanismo real para detectar unidades, funciones de color, espacios de interpolación y palabras clave nuevas: metes la novedad dentro de una propiedad que la acepte y preguntas por la declaración entera.

⚠️
Un positivo no promete comportamiento

@supports responde sobre el analizador, no sobre la implementación. Un navegador puede analizar perfectamente una declaración y aplicarla mal, aplicarla solo en algunos contextos, o tener un bug que la haga inservible. Históricamente ha pasado varias veces: motores que aceptaban position: sticky con implementaciones parciales, o que analizaban contain sin implementar todos los tipos. La condición dice “sé leer esto”; no dice “hago lo que esperas”.

Elegir bien la declaración de prueba

Como la respuesta depende de qué escribas dentro de los paréntesis, la calidad de la detección es la calidad de la pregunta. Dos errores frecuentes.

Preguntar por algo más antiguo que lo que quieres. Detectar una característica preguntando por una propiedad relacionada que llegó antes produce falsos positivos permanentes. La propiedad relacionada existe, la condición da verdadero, y la característica que de verdad te importaba no está.

Preguntar por algo que el navegador acepta y tira. Algunos valores se analizan y luego se ignoran en el contexto concreto donde los usas. La declaración de prueba tiene que ser la misma que vas a escribir después, con el mismo valor. Si vas a usar grid-template-columns: subgrid, pregunta por eso exactamente, no por display: grid.

La regla práctica: la condición debe ser la declaración más específica del bloque que protege. Si dentro del bloque hay una línea que es la que de verdad no existía antes, esa es la que va entre paréntesis.

/* mal: la condicion es mas vieja que la mejora */
@supports (display: grid) {
  .tarjeta > * { grid-template-columns: subgrid; }
}

/* bien: pregunta por lo que de verdad usas */
@supports (grid-template-columns: subgrid) {
  .tarjeta > * { grid-template-columns: subgrid; }
}

La misma pregunta desde JavaScript

CSS.supports() usa el mismo evaluador y admite dos firmas.

// dos argumentos: propiedad y valor, sin parentesis
CSS.supports('display', 'grid');              // true

// un argumento: una condicion completa, con parentesis
CSS.supports('(display: grid)');              // true
CSS.supports('(display: grid) or (display: flex)');
CSS.supports('selector(:has(a))');
CSS.supports('not (width: 1foo)');            // true

La firma de dos argumentos es la más segura porque no tienes que preocuparte de la gramática de condiciones; la de un argumento es la única que permite combinaciones y funciones como selector().

El uso legítimo es decidir en JavaScript si merece la pena cargar un polyfill o registrar un observador. El uso ilegítimo, y muy común, es replicar en JS una decisión que el CSS ya habría tomado solo: si el navegador va a descartar la declaración de todas formas, no necesitas preguntarle antes.

La detección es un síntoma, no una herramienta

Cada @supports que escribes es una bifurcación permanente en tu hoja de estilos: dos caminos que hay que probar, dos que hay que mantener y dos que alguien tendrá que unificar cuando el soporte llegue. El coste no está en los bytes, está en que una condición divide el espacio de estados de tu CSS en dos, y esa división se multiplica con cada condición nueva. Antes de escribir una, haz la pregunta que casi nadie hace: ¿qué pasa exactamente si no hago nada? En una fracción sorprendente de los casos la respuesta es “el navegador descarta la declaración y queda algo perfectamente usable”, y entonces la condición sobra. Las características de CSS que se diseñan bien son precisamente las que degradan solas: gap en un contenedor sin soporte simplemente no separa, aspect-ratio sin soporte deja la caja a su tamaño natural, un color() fuera de gamut se mapea. Los diseñadores de la especificación dedican reuniones enteras a que la degradación sea automática, justamente para que no tengas que detectar. Usar @supports cuando no hace falta es tirar ese trabajo a la basura y quedarte con el mantenimiento.