Los encabezados fijos son un patrón de IU común, pero a menudo necesitan cambiar su apariencia cuando se "fijan" para mantener la legibilidad o ahorrar espacio. Las soluciones tradicionales requerían objetos de escucha de desplazamiento de JavaScript, lo que puede causar problemas de rendimiento. El CSS moderno te permite controlar esto de forma nativa con las consultas de estado de desplazamiento.
Pasos de implementación
1. Crea el contenedor fijo
Necesitas un contenedor que actúe como el elemento fijo y el contenedor para la consulta de estado de desplazamiento.
<!-- Wrap in a section to define the containment area for the sticky element -->
<div class="section">
<div class="sticky-container">
<div class="sticky-header">
Section Header
</div>
</div>
<div class="content">
<!-- Content goes here -->
</div>
</div>
2. Aplica CSS para el comportamiento fijo y el tipo de contenedor
Establece container-type: scroll-state en el elemento position: sticky, no en su elemento principal desplazable. Especifica siempre un container-name, ya que estas consultas pueden estar anidadas y los contenedores sin nombre entrarán en conflicto. También puedes combinarlo con búsquedas de tamaño, p.ej., container-type: scroll-state inline-size.
.sticky-container {
position: sticky;
top: 0;
/* MANDATORY: Always specify a container name to avoid collisions if CQs are nested */
container-type: scroll-state;
container-name: section-header;
z-index: 10;
}
.sticky-header {
/* Base styles for the header */
background: #f0f0f0;
padding: 20px;
transition: background-color 0.3s, box-shadow 0.3s;
}
Importante: Los descendientes del contenedor de estado de desplazamiento consultan el estado de desplazamiento. Los estilos de la consulta @container no se aplicarán al .sticky-container en sí, sino a sus elementos secundarios (como .sticky-header). No puedes aplicar diseño al elemento contenedor en sí con su propia consulta de estado de desplazamiento.
3. Cómo abordar el estado de estancamiento
Usa la consulta @container scroll-state(...) para aplicar estilos cuando el encabezado esté fijo.
/* Apply styles when the named container is stuck at the top */
@container section-header scroll-state(stuck: top) {
.sticky-header {
background: #0056b3;
color: white;
box-shadow: 0 4px 6px rgb(0 0 0 / 0.15);
}
}
Sugerencia: También puedes usar propiedades lógicas, como stuck: inset-block-start o stuck: inset-inline-start, para admitir mejor la internacionalización (i18n) y los diseños de derecha a izquierda (RTL) consultando los bordes relativos al flujo en lugar de los físicos.
Notas sobre los cambios de dimensiones
Si no se inhabilita el anclaje de desplazamiento, cambiar las propiedades que afectan el diseño (altura, padding, tamaño de fuente) cuando se fija puede causar parpadeos visuales. El anclaje de desplazamiento en el contenido en flujo debajo del elemento fijo ajusta el desplazamiento para compensar el cambio de diseño, lo que hace que el elemento vuelva a salir de su posición fija y activa una oscilación.
Inhabilita el anclaje de desplazamiento en el elemento superior del elemento fijo para evitar esto:
.section {
overflow-anchor: none;
}
Aplica overflow-anchor: none al elemento superior directo del elemento position: sticky, no a :root ni a body, lo que inhabilitaría el anclaje de desplazamiento para toda la página. Con esto, puedes cambiar libremente cualquier propiedad en el estado fijo, incluidas las propiedades del modelo de caja y las transformaciones.
NO uses overflow-anchor: none para los elementos que se encuentran en la parte inferior. El anclaje de desplazamiento solo compensa los cambios de diseño por encima de la posición de desplazamiento actual, por lo que no tiene efecto en los elementos fijos en la parte inferior. Evita cambiar las propiedades del modelo de caja en el estado fijo para los encabezados fijos en la parte inferior.
Estrategias de resguardo
Las consultas de estado de desplazamiento son una mejora progresiva. En los navegadores que no admiten container-type: scroll-state, el encabezado seguirá fijado en la parte superior (debido a position: sticky), pero no se transformará visualmente. Para la mayoría de los casos de uso, este es el enfoque recomendado.
Si la adherencia no es esencial, pero position: sticky sin un estilo diferente sería una experiencia peor que no tener adherencia, también puedes unir position: sticky y la declaración relacionada en una consulta @supports:
.sticky-container {
@supports (container-type: scroll-state) {
position: sticky;
top: 0;
container-type: scroll-state;
container-name: section-header;
z-index: 10;
}
}
Sugerencia: Si el diseño "fijo" requiere un color de fondo diferente para mejorar la legibilidad, considera configurar el diseño de modo que el estilo predeterminado funcione en todas partes (mejora progresiva). Si debes usar un resguardo, puedes controlar el comportamiento de position: sticky dentro de una consulta de @supports (container-type: scroll-state).
Si la transformación visual es absolutamente fundamental para el diseño (p.ej., el estado fijo introduce un fondo o una compactación sin los cuales el contenido es ilegible), puedes implementar un resguardo de JavaScript sólido con IntersectionObserver. Debes duplicar tus estilos de CSS en una clase .is-stuck. El siguiente polyfill genérico verifica getComputedStyle para recorrer hacia arriba y encontrar el contenedor de desplazamiento correcto (que se establece de forma predeterminada en el viewport), lo que coincide con el comportamiento de @container scroll-state(stuck: top):
function getScrollParent(node) {
if (node == null || node === document.body || node === document.documentElement) {
return null; // default to viewport
}
if (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth) {
const overflow = getComputedStyle(node).overflow;
if (overflow !== 'visible' && overflow !== 'clip') {
return node;
}
}
return getScrollParent(node.parentNode);
}
document.querySelectorAll('.sticky-container').forEach(container => {
const root = getScrollParent(container);
const topOffset = parseFloat(getComputedStyle(container).top) || 0;
const observer = new IntersectionObserver(
([e]) => {
// Toggle the fallback class on the sticky header container
e.target.classList.toggle('is-stuck', e.intersectionRatio < 1);
},
{
root: root,
threshold: [1],
rootMargin: `-${topOffset + 1}px 0px 0px 0px`
}
);
observer.observe(container);
});
Nota: Este patrón genérico de IntersectionObserver también se puede usar como polyfill para la consulta scroll-state(scrollable).