Detectar con @supports
La consulta de características correcta para timelines de scroll, por qué el descarte silencioso no basta aquí, y la detección equivalente desde JavaScript.
En CSS, la mayoría de las veces no hace falta detectar nada: el motor descarta las declaraciones que no entiende y dos declaraciones consecutivas de la misma propiedad son un plan alternativo completo. Las animaciones dirigidas por scroll son el caso donde esa técnica no sirve, porque lo que cambia no es un valor sino una estrategia entera repartida por varias propiedades. Aquí @supports no es una opción defensiva: es la herramienta.
- Escribir la consulta
@supportscorrecta y saber qué prueba exactamente. - Explicar por qué el descarte silencioso de declaraciones no resuelve este caso.
- Combinar
@supportscon@mediaen el orden que corresponde. - Detectar lo mismo desde JavaScript cuando de verdad haga falta.
La consulta correcta
@supports (animation-timeline: scroll()) {
/* Aqui dentro, las timelines de scroll existen */
}
Esa es la consulta. Merece la pena entender qué está probando, porque hay variantes que parecen equivalentes y no lo son.
Una consulta @supports con un par de propiedad y valor pregunta al motor si entiende esa declaración completa, es decir, si la propiedad existe y si el valor es válido para ella. Probar animation-timeline: scroll() prueba las dos cosas a la vez, que es justo lo que quieres.
Estas variantes son peores y conviene saber por qué:
@supports (animation-timeline: auto)prueba solo que la propiedad existe con un valor genérico. Un motor podría reconocer la propiedad sin implementar la notación funcional. Es una prueba más débil.@supports (animation-range: entry)prueba una pieza distinta del sistema. Como todas las piezas se enviaron juntas en los dos motores que las tienen, en la práctica da el mismo resultado, pero estás probando algo que no es lo que vas a usar.@supports selector(...)no aplica: no hay selector implicado.
Si usas específicamente view() y quieres ser riguroso, prueba view():
@supports (animation-timeline: view()) {
.tarjeta { /* ... */ }
}
Y si un mismo bloque usa las dos formas, la consulta compuesta es legítima y explícita:
@supports (animation-timeline: scroll()) and (animation-timeline: view()) {
/* ... */
}
La forma negada existe y sirve para el camino contrario, cuando el fallback necesita declaraciones propias en lugar de limitarse a ser el estado base:
@supports not (animation-timeline: scroll()) {
.progreso { display: none; }
}
Como norma, prefiere la forma positiva. Un @supports not deja el CSS con la lógica invertida y obliga a razonar en negativo, y sobre todo tiene un problema de futuro: cuando Firefox implemente las timelines, el bloque negado se queda ahí sin hacer nada y nadie recuerda si se puede borrar. Un bloque positivo, en cambio, simplemente pasa a aplicarse en más sitios.
Por qué el descarte silencioso no basta
En CSS, la técnica más barata de plan alternativo es escribir dos declaraciones seguidas y dejar que el motor se quede con la última que entienda. Funciona porque el descarte es por declaración. Y aquí falla, por un motivo estructural que conviene ver:
/* Esto NO resuelve nada */
.tarjeta {
opacity: 1;
opacity: 0; /* los dos motores la entienden */
animation: revelar linear both;
animation-timeline: view(); /* solo esta se descarta */
}
El motor sin timelines descarta exactamente una declaración: la última. Todo lo demás —incluida la opacidad cero— se aplica sin problema, porque opacity: 0 es CSS perfectamente válido en todos los navegadores desde hace veinte años. El descarte silencioso solo ayuda cuando la declaración problemática es la que quieres condicionar, y aquí la declaración problemática es la inofensiva.
Este es el criterio general: el descarte por declaración sirve cuando lo que varía es un valor; @supports sirve cuando lo que varía es una estrategia. Las timelines de scroll cambian la estrategia, porque el estado inicial, el estado final y la fuente del progreso son tres decisiones acopladas repartidas en varias declaraciones. No hay forma de expresar ese acoplamiento con orden de declaraciones.
Combinar con media queries
@supports y @media se anidan en cualquier orden y el resultado es el mismo, pero el orden que elijas comunica una intención distinta y afecta a cuánto CSS hay que repetir.
/* Preferible: el soporte fuera, las condiciones de usuario dentro */
@supports (animation-timeline: view()) {
.revelar {
animation: revelar linear both;
animation-timeline: view();
animation-range: entry;
}
@media (prefers-reduced-motion: reduce) {
.revelar { animation-name: revelar-suave; }
}
}
Poner @supports fuera agrupa todo lo que depende de la capacidad en un solo bloque, y dentro se organizan las variaciones. La estructura inversa —un @media que contiene dos @supports— obliga a repetir la consulta y multiplica los sitios donde puede desincronizarse.
Con capas de cascada, una recomendación adicional: mete la mejora en su propia capa. Así queda explícito que es una capa opcional y que su orden respecto al resto está decidido a propósito.
@layer base, movimiento;
@layer base {
.revelar { opacity: 1; transform: none; }
}
@layer movimiento {
@supports (animation-timeline: view()) {
.revelar {
animation: revelar linear both;
animation-timeline: view();
animation-range: entry;
}
}
}
Hay un límite duro que conviene conocer antes de estrellarse contra él. @supports responde a “¿entiende el motor esta sintaxis?” y no responde a “¿va a funcionar esto?”. Son preguntas distintas y la segunda no es contestable con CSS. Un navegador que implementa timelines de scroll perfectamente puede dejarte una animación sin ningún efecto porque el scroller no desborda todavía, porque el eje pedido no scrollea, porque el nombre no resuelve, o porque el sujeto tiene position: fixed. En todos esos casos @supports dice que sí, la declaración es válida, y el elemento se queda con su estilo base sin que nada avise. La consecuencia práctica es la más importante de todo el nivel: la mejora progresiva de las timelines de scroll no puede depender solo de @supports. El estado base tiene que ser seguro de todas formas, porque la lista de motivos por los que una animación de scroll puede no aplicar valores es mucho más larga que “el navegador no la implementa”, y ninguno de los otros motivos es detectable. Dicho de otro modo: si tu página solo se ve bien cuando la timeline está activa, tienes un bug latente incluso en Chrome. Y desde JavaScript tampoco hay salida limpia: puedes preguntar CSS.supports(), que responde lo mismo que @supports, y puedes leer animation.timeline.currentTime para ver si es null, pero eso te obliga a mantener lógica imperativa sincronizada con el CSS, que es exactamente lo que las timelines venían a eliminar.
La detección desde JavaScript
Cuando de verdad haga falta —para decidir si cargar un polyfill, básicamente— la comprobación equivalente es CSS.supports(), que acepta las mismas dos formas que @supports:
const hayTimelines =
CSS.supports('animation-timeline', 'scroll()') ||
CSS.supports('animation-timeline: scroll()');
if (!hayTimelines) {
await import('scroll-timeline-polyfill');
}
Las dos llamadas hacen lo mismo con sintaxis distinta; una sola basta, y la doble comprobación solo tiene sentido si te preocupan implementaciones antiguas de la propia función. Para el caso del polyfill, la importación dinámica es la forma correcta: el navegador que sí tiene timelines no descarga nada.
Existe una segunda vía, comprobar la presencia del constructor:
const hayTimelines = 'ScrollTimeline' in window;
Es más corta y más frágil, porque un polyfill que ya se haya cargado define el constructor y hace que la comprobación devuelva true aunque el soporte nativo no exista. Si la usas para decidir si cargar el polyfill, tienes una condición que se invalida a sí misma. CSS.supports() no tiene ese problema: pregunta por el motor de CSS, y un polyfill de JavaScript no puede mentir al respecto.