La propiedad position:sticky

facebook-svg gplus-svg twitter-svg

Pregunta

Quiero que una columna lateral de mi página se quede fija al alcanzar un cierto umbral de desplazamiento. ¿Cómo puedo hacerlo?

Respuesta:

Existen dos maneras de hacerlo:
- Utilizando JavaScript o
- Utilizando CSS

El diseño de página

El diseño de página es minimalista. Un elemento <main>, que se encuentra centrado en el medio de la página, contiene un párrafo inicial <p>. . . </p> y a continuación vienen las dos columnas: un elemento <section> flotado a la izquierda: float: left; y un elemento <aside> flotado a la derecha: float: right;. Al final un elemento <br> se encarga de limpiar los floats: br{clear:both;}

  <main>
  <p>. . . </p>
  <section></section>
  <aside></aside>
  <br>
  </main>

El CSS es bastante sencillo:


* {
  box-sizing: border-box;
}
main {
  width: 50em;
  margin: 0 auto;
  outline:1px solid red;
}
section {
  width: 32.5em;
  float: left;
  outline:1px solid #5ab150;
}
aside {
  width: 17.5em;
  padding-left:1em;
  float: right;
  outline:1px solid gold;
}
h4{
  text-align:center;
  font-size:120%;
}
p{
  padding:0 .5em;
}
br{
  clear:both;
}

Vealo en codepen:

See the Pen Layout para position: sticky; by Gabi (@enxaneta) on CodePen.

Utilizando JavaScript

Para obtener las coordenadas de un elemento podemos utilizar el método getBoundingClientRect() que devuelve algo así:

ClientRect
  bottom: 772.125
  height: 539.0625
  left: 811
  right: 1091
  top: 233.0625
  width: 280
  __proto__: ClientRect

Por ejemplo en este caso la distancia entre el elemento y el margen superior del documento es top: 811 ( pixeles ).

Si hacemos scroll esta distancia NO cambia, pero podemos saber cuanto se ha desplazado utilizando la propiedad scrollY de window.

window.scrollY

La pregunta es: "Quiero que una columna lateral de mi página se quede fija al alcanzar un cierto umbral de desplazamiento. ¿Cómo puedo hacerlo?"
Primero tenemos que decidir el umbral de desplazamiento, que en este caso es:

var umbralDeDesplazamiento = aside.getBoundingClientRect().top;

O sea: cuando, al hacer scroll, la ventana ( window ) toca el umbral de desplazamiento, quiero que este elemento se quede parrado.

  // al hace scroll
  window.addEventListener("scroll", function(evt) {
  // si la ventana toca toca el umbral de desplazamiento  
  if (window.scrollY >= umbralDeDesplazamiento) {
  // el elemento aside se queda parrado
  aside.style.position = "fixed";  
  aside.style.left = sectionRight + "px";           
  } else {
  aside.style.position = "static"; 
  }
}, false);
El problema de position: fixed;

Cuando en CSS un elemento tiene position: fixed; este elemento se posiciona relativamente a la ventana, sin tener en consideración los demás elementos. Así que tenemos que calcular la posición desde la izquierda, que en este caso coincide con la derecha del elemento <section>.

  var sectionRight = section.getBoundingClientRect().right; 
  . . . . . 
  
  // al hace scroll
  window.addEventListener("scroll", function(evt) {
  // si la ventana toca toca el umbral de desplazamiento  
  if (window.scrollY >= umbralDeDesplazamiento) {
  // el elemento aside se queda parrado
  aside.style.position = "fixed";  
  //la posición izquierda de aside coincide con la derecha de section
  aside.style.left = sectionRight + "px";
  } else {
  // de lo contrario aside se queda en su posición
  aside.style.position = "static"; 
  }
}, false);
El evento resize

Y parece funcionar. Pero si lo dejamos así, todo se vendrá abajo al redimensionar la página. El culpable es la propiedad left del elemento posicionado fixed, ya que la calculamos relativo a la posición del elemento section, que cambia "on resize":

aside.style.left = sectionRight + "px";

Para que esto no pase tendremos que tomar en consideración el evento resize. Sin embargo el evento resize se dispara con una frecuencia muy alta, y esto lo hace inapropiado para tareas complicadas como recalcular posiciones de elementos DOM.  Para poder hacerlo es recomendable utilizar el método setTimeout o requestAnimationFrame para reducir la frecuencia con la cual se dispara el evento resize.

setTimeout(function() {
    init();
    addEventListener('resize', init, false);
  }, 15);

Donde la función init() calcula la posición izquierda del elemento aside:

function init(){
  sectionRight = section.getBoundingClientRect().right;
  aside.style.left = sectionRight + "px";
}

Así es como queda el JavaScript:


var aside = document.querySelector("aside");
var section = document.querySelector("section");

var umbralDeDesplazamiento = aside.getBoundingClientRect().top;
var sectionRight = section.getBoundingClientRect().right;

window.addEventListener("scroll", function(evt) {
  if (window.scrollY >= umbralDeDesplazamiento) {
    aside.style.position = "fixed";  
    aside.style.left = sectionRight + "px";           
  } else {
    aside.style.position = "static"; 
  }
}, false);


function init(){
sectionRight = section.getBoundingClientRect().right;
aside.style.left = sectionRight + "px";
}


setTimeout(function() {
  init();
  addEventListener('resize', init, false);
}, 15);		

Vea este ejemplo en codepen.

See the Pen position: sticky en JS by Gabi (@enxaneta) on CodePen.

Utilizando CSS

Para hacer lo mismo utilizando solo CSS simplemente necesitamos añadir estas líneas de código:

aside {
    width: 35%;
    padding-left:1em;
    float: right;
    position: -webkit-sticky;
    position: sticky;
    top: 0px; 
    outline:1px solid gold;
}

Esta línea de código: top: 0px; dice al navegador que el elemento aside tiene que quedarse clavado cuando toca el limite superior de la ventana.

Vea este ejemplo en codepen:

See the Pen position: sticky en CSS by Gabi (@enxaneta) on CodePen.

El único inconveniente es que el valor sticky no está todavía bien soportado en todos los navegadores

Sin embargo existen polyfils que pueden ser útiles:
- filamentgroup/fixed-sticky
- position: sticky polyfill