5

Quiero saber si un elemento se encuentra visible en un momento dado en el viewport, es decir en la ventana de visualización del navegador, usando JavaScript/jQuery.

Por esta otra pregunta, sé que con .is(':visible') puedo averiguar si ese elemento está visible o no, pero eso no quiere decir que este dentro de la ventana de visualización (y por tanto invisible para el usuario), que es lo que yo quiero.

Por ejemplo: En el siguiente código se comprueba si un elemento está visible cuando se pulsa el botón; pero si realizamos scroll, bajamos hasta abajo de la página y volvemos a pulsar en el botón, nos sigue diciendo que sí lo está aunque el usuario ya no puede verlo.

$("button").on("click", function() {
  alert($("#holamundo").is(":visible"));
});
button {
  position:fixed;
  top:5px;
  right:5px;
}

p {
  margin-top: 800px;
}
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

<button>¿Está "Hola Mundo" visible?</button>

<div id="holamundo">Hola Mundo</div>

<p>.</p>
<p>.</p>

¿Existe alguna función propia o cómo se haría en JavaScript/jQuery para detectar si un elemento está visible Y en la ventana de visualización?

Alvaro Montoro
  • 48,157
  • 26
  • 100
  • 179

2 Answers2

4

Podríamos validar esto accediendo primero a los valores de la "ventana de visualización" , para esto se hace uso de scrollTop del objeto window para saber cuando se desplazó hacía abajo y el limite lo obtenemos de la suma del valor devuelvo por scrollTop + height de la ventana

Luego tendríamos que realizar el mismo procedimiento para el elemento , para esto emplearemos offset para las coordenadas y acceder a la propiedad top para luego obtener la altura del elemento también con height

Ya con estos valores validaríamos comparando , la función quedaría así:

function esVisible(elem){
    /* Ventana de Visualización*/
    var posTopView = $(window).scrollTop();
    var posButView = posTopView + $(window).height();
    /* Elemento a validar*/
    var elemTop = $(elem).offset().top;
    var elemBottom = elemTop + $(elem).height();
    /* Comparamos los dos valores tanto del elemento como de la ventana*/
    return ((elemBottom < posButView && elemBottom > posTopView) || (elemTop >posTopView && elemTop< posButView));
}

$("button").on("click", function() {
  var ele = document.getElementById('holamundo');
  console.log(esVisible(ele));
});
button {
  position:fixed;
  top:5px;
  right:5px;
}

p {
  margin-top: 800px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button>¿Está "Hola Mundo" visible?</button>
<div id="holamundo">Hola Mundo</div>
<p>.</p>
<p>.</p>

Update

Como recomendación de @Alvaro Montoro , Con el ejemplo anterior funcionaría pero height no tomará en cuenta el padding como sí lo hace outerheight

function esVisible(elem){
    /* Ventana de Visualización*/
    var posTopView = $(window).scrollTop();
    var posButView = posTopView + $(window).height();
    /* Elemento a validar*/
    var elemTop = $(elem).offset().top;
    var elemBottom = elemTop + $(elem).outerHeight();
    /* Comparamos los dos valores tanto del elemento como de la ventana*/
    return ((elemBottom < posButView && elemBottom > posTopView) || (elemTop >posTopView && elemTop< posButView));
}

$("button").on("click", function() {
  var ele = document.getElementById('holamundo');
  console.log(esVisible(ele));
});
button {
  position:fixed;
  top:5px;
  right:5px;
}

p {
  margin-top: 800px;
}
#holamundo{
    padding: 120px;
    background: #ccc;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button>¿Está "Hola Mundo" visible?</button>

<div id="holamundo">Hola Mundo</div>

<p>.</p>
<p>.</p>

Ejemplo empleando solo JavaScript

function esVisible(elem){
    var posTopView = window.scrollY;
    var posButView = posTopView + window.innerHeight;
    var elemTop = elem.offsetTop;
    var elemBottom = elemTop + elem.offsetHeight;
    return ((elemBottom < posButView && elemBottom > posTopView) || (elemTop >posTopView && elemTop< posButView));
}

document.getElementById('btn').addEventListener("click", function(){
    var ele = document.getElementById('holamundo');
    console.log(esVisible(ele));
});
button {
  position:fixed;
  top:5px;
  right:5px;
}

p {
  margin-top: 800px;
}
<button id="btn">¿Está "Hola Mundo" visible?</button>
<div id="holamundo">Hola Mundo</div>
<p>.</p>
<p>.</p>

Referencia en SO

Update

Otra opción sería utilizar la API IntersectionObserver() para saber si el elemento está visible o no.

  • Crear el objeto de opciones con tres valores root , está propiedad determina el elemento donde se validará la visibilidad del elemento a observar,por defecto toma el viewport del navegador rootMargin, esta propiedad determina el margen que se incluirá en la evaluación de la visibilidad threshold , esta propiedad determina el porcentaje de visibilidad que se desea observar, el valor por defecto es 0 es decir que tan pronto como sea visible (1px mínimo), y 1 cuando el elemento esté completamente visible. esta propiedad se pueden pasar más de 1 parámetro, revisar la documentación. :)

  • Crear la instancia de IntersectionObserver, como primer parámetro un callback y como segundo parámetro el objeto de opciones

  • Asignar el observer al o los elementos a evaluar. Para el ejemplo solo uno con el id holamundo

Dentro del callback tendrá el parámetro entries que hará referencia a los elementos observados, (el ejemplo el elemento sería el indice 0), a través de isIntersecting verificamos si está visible teniendo en cuenta las opciones, a partir de eso podemos realizar acciones según sea el caso.

function callback(entries,observer){
  if(entries[0].isIntersecting){//verificamos si actualmente es visible
    console.log("El elemento ya está visible...");
  }else{
    console.log("El elemento no es visible.");
  }
}
var observer = new IntersectionObserver(callback, {});

const element = document.querySelector('#holamundo');
observer.observe(element);
.boxMargin {
  width: 300px;
  height: 400px;
  border : 1px solid #aec;
}
<div class="boxMargin"></div>
<div class="boxMargin"></div>
<div id="holamundo">Hola Mundo</div>
<div class="boxMargin"></div>
<div class="boxMargin"></div>
Dev. Joel
  • 23,229
  • 3
  • 25
  • 44
  • 1
    Esta solución parece tener algún problema. Si muevo un poco el scroll, el elemento se sigue viendo, pero obtengo `false`. Ejemplo: http://i.imgur.com/sfR4uIS.gif – Alvaro Montoro Jun 26 '17 at 03:12
  • Crei que el problema es que sólo comparas la parte de arriba del elemento, pero no la parte baja. Cambiando la lógica para que incluya las dos parece funcionar: `return ((elemTop > posTopView && elemTop < posButView) || (elemBottom > posTopView && elemBottom < posButView) )` – Alvaro Montoro Jun 26 '17 at 03:24
  • @AlvaroMontoro en teoría funciona. ¿Cree que se puede mejorar algo la función? o algún caso extraordinario se le escapa a dicha función? Ah y gracias por la observación no me había fijado bien en ese detalle. – Dev. Joel Jun 26 '17 at 03:34
  • Se ve bien. Lo único que consideraría quizás sería usar `innerHeight` o `outerHeight` para que se incluyera el padding. Pero eso sería para casos más particulares – Alvaro Montoro Jun 26 '17 at 03:47
  • 1
    Gracias por el update @Dev.Joel, con eso soluciono el problema – Theia Jul 19 '17 at 19:53
2

Yo hice un modulo de npm (disponible en la cdn jsdelivr) llamado scroll-utility que anade muchas facilidades para trabajar con el scroll, incluyendo saber si un elemento esta visible dentro de otro o en la ventana.
Es libre de dependencias, por lo que no necesita de jQuery para funcionar.

$("button").on("click", function() {
const relativePosition = new ScrollUtility.Scroll().getRelativeElementPosition("#holamundo")
let text = "" 
if (relativePosition < -1) {
  text = "#holamundo no esta visible (arriba del viewport)"
}
if (relativePosition > -1 && relativePosition < 0) {
  text = "#holamundo esta parcialmente visible (pegado arriba)"
}
if (relativePosition > 0 && relativePosition < 1) {
  text = "#holamundo esta completamente visible"
  if (relativePosition > 0.4 && relativePosition < 0.6) {
    text = "#holamundo esta casi centrado en el viewport"
  }
}
if (relativePosition > 1 && relativePosition < 2) {
  text = "#holamundo esta parcialmente visible (pegado abajo)"
}
if (relativePosition > 2) {
  text = "#holamundo no esta visible (debajo del viewport)"
}
alert(text)
});
button {
  position:fixed;
  top:5px;
  right:5px;
}

p {
  margin-top: 800px;
}

#holamundo {
  background: gray;
  height: 50vh;
  margin-top: 400px;
}
<script src="https://cdn.jsdelivr.net/npm/scroll-utility@4.0.0/dist/scroll-utility.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

<button>¿Está "Hola Mundo" visible?</button>

<div id="holamundo">Hola Mundo</div>

<p>.</p>
<p>.</p>
Theia
  • 786
  • 2
  • 8
  • 24