El ecosistema de CSS está evolucionando rápidamente, introduciendo herramientas nativas que antes requerían complejas soluciones de JavaScript. En este artículo exploramos dos de las características más potentes: Anchor Positioning y Scroll-driven Animations.
Resumen de Capacidades: ¿Por qué usar estas nuevas APIs?
CSS Anchor Positioning
Esta especificación resuelve un problema histórico: posicionar un elemento relativo a otro sin depender de la estructura del DOM.
- Independencia del DOM: Permite conectar elementos visualmente aunque estén en contenedores HTML completamente separados.
- Rendimiento: Elimina la necesidad de cálculos de posición en JavaScript (adiós a listeners de
resizeoscrollcostosos). - Semántica: Mantiene el HTML limpio, permitiendo que los popups o tooltips vivan en la capa superior (
top-layer) sin hacks dez-index.
Scroll-driven Animations (animation-timeline)
Permite vincular el progreso de una animación CSS directamente al desplazamiento de una barra de scroll.
- Optimización: Al ejecutarse en el hilo del compositor, estas animaciones no bloquean el hilo principal, garantizando 60fps incluso con JavaScript pesado ejecutándose.
- Simplicidad: Reemplaza la necesidad de
IntersectionObserverpara efectos de “aparición al hacer scroll”. - Control: Ofrece funciones como
view()para animar según la visibilidad del elemento en el viewport.
CSS Anchor Positioning
Tendremos un elemento que usaremos como punto de referencia de ancla. Daremos el nombre que nos interese mediante el atributo “anchor-name” (el nombre dado debe empezar por ”—”).
.anchor {
anchor-name: --anchor;
}
Y ahora otro elemento que será anclado al punto definido anteriormente. Lo más importante de este sistema de anclas es que no necesitamos relación padre/hijo para establecer dicha relación, pueden ser nodos en cualquier punto del DOM.
.conditional-sticky {
position: fixed;
z-index: 1;
top: anchor(--anchor bottom);
left: anchor(--anchor left);
right: anchor(--anchor right);
}
La estructura HTML de los elementos es la siguiente:
<nav class="wrapper-nav anchor">
<div class="nav">
<!-- ... -->
</div>
</nav>
<main class="main">
<div class="card observe-visibility" id="go-to">
<div class="card__content"></div>
<div class="conditional-sticky">
<div class="conditional-sticky__inner">
Don't miss the CSS anchor positioning feature + animation-timeline
<a href="#go-to" class="link">
<span>let's go</span> <span class="arrow">↓</span>
</a>
</div>
</div>
</div>
<div class="card">
<div class="card__content"></div>
</div>
</main>
Con todo lo anterior, hemos usado CSS Anchor Positioning que todavía está en modo experimental, pero que puede probarse habilitando las flags experimentales en Chrome:
chrome://flags/#enable-experimental-web-platform-features
Un recurso bastante bueno en el que se ve toda la potencia de esta nueva Feature CSS Anchor Positioning es el siguiente.
Animation Timeline: Animaciones al hacer Scroll
Quería darle una vuelta a lo anterior y hacer una cosa que hice con Javascript en otro proyecto.
La idea es tener un elemento Sticky debajo del menú de navegación (que también es Sticky) que sea condicionado su visibilidad a que un elemento cualquiera del DOM esté dentro de Viewport. Hacer esto es sencillo hacerlo con Javascript usando al API IntersectionObserver, pero querí probar a no usarlo.
El elemento que será sticky es el que comentamos antes que posicionamos con “CSS Anchor Positioning” .conditional-sticky:
<div class="card observe-visibility" id="go-to">
<div class="card__content"></div>
<div class="conditional-sticky">
<div class="conditional-sticky__inner">
Don't miss the CSS anchor positioning feature + animation-timeline
<a href="#go-to" class="link">
<span>let's go</span> <span class="arrow">↓</span>
</a>
</div>
</div>
</div>
Mediante Custom Property estableceremos su valor de display (que más adelante modificaremos):
.conditional-sticky {
display: var(--observe-visibility);
}
Dicha Property la definimos con CSS:
@property --observe-visibility {
initial-value: block;
inherits: true;
syntax: '<custom-ident>';
}
Su valor irá cambiando mediante @keyframes:
@keyframes observe-visibility {
1%,
100% {
--observe-visibility: none;
}
}
Esta @keyframes esta asociada al elemento de pantalla que queramos vigilar:
.observe-visibility {
animation: observe-visibility linear;
animation-timeline: view();
animation-range: cover 0%;
}
Los funciones de animación deben ser “linear”, ya que no está asociadas a tiempos como tradicionalmente han sido. En este caso, está basado en el scroll que se hace en la página. animation-timeline nos ofrece dos funciones: scroll() y view(), en este caso me interesa la segunda. Sobre el rango, tenemos múltitud de opciones: entradas/salidas de pantalla, porcentajes/píxeles/…
Recomiendo este árticulo para más info de Bramus con multitud de ejemplos, son una pasada.
Extra: detectar si un elemento está por encima o por debajo del Viewport
Dentro del elemento sticky tengo un vínculo con una flecha que cambia de dirección: hacía abajo si el elemento de referencia se encuentra por la cara inferior del scroll y hacía arriba si está por la cara superior. Te dejo el snipet de código apra que lo pienses un poco :)
<a href="#go-to" class="link">
<span>let's go</span> <span class="arrow">↓</span>
</a>
@property --detect-direction {
initial-value: 0deg;
inherits: true;
syntax: '<angle>';
}
.arrow {
rotate: var(--detect-direction);
}
.observe-visibility {
animation: detect-direction linear forwards;
animation-timeline: view();
animation-range: exit;
}
@keyframes detect-direction {
to {
--detect-direction: 180deg;
}
}
Nota: no me preocupé por la dirección correcta del arrow cuando esté el elemento observado en pantalla, ya que su padre estará oculto :)
Recomiendo bucear en las cosas que hacen y publican Bramus y de Jhey, cosas chulísimas!