La propiedad position:sticky
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