Event listener for when element becomes visible?
Asked Answered
E

11

213

I am building a toolbar that is going to be included into a page. the div is going to be included in will default to display: none.
Is there any way I can put an event listener on my toolbar to listen for when it becomes visible so it can initialize?
or will I have to pass it a variable from the containing page?

Everyone answered 22/9, 2009 at 19:34 Comment(5)
Does this answer help? #1397751Monometallic
@kangax, thank you. But since its not widely implemented I think I'm going to scratch the whole event listener idea and go a different route.Everyone
See this answer for an implementation of an "onVisible" event in JavaScript: https://mcmap.net/q/94574/-how-to-implement-an-39-onvisible-39-event-in-javascriptSev
possible duplicate of How to implement an 'onVisible' event in Javascript?Warehouse
Could see this please. #45430246Bield
E
-1

Javascript events deal with User Interaction, if your code is organised enough you should be able to call the initialising function in the same place where the visibility changes (i.e. you shouldn't change myElement.style.display on many places, instead, call a function/method that does this and anything else you might want).

Expect answered 22/9, 2009 at 20:47 Comment(7)
But how would you be able to detect the change in visibility (i. e., call a function as soon as an element becomes visible)?Sev
I thiiiink you can't do that. What you would do instead of trying to detect the change, is knowing exactly where it is provoked, and put your call there. If the change in visibility is not something your own code is directly responsible for (e.g. some lib) and the logic by which that happens is very contrived, I guess you're out of luck? :)Expect
I downvoted this question because despite all the sense it makes, it's irrelevant to the actual issue. (if I display none on a frame, all children elements become invisible)Will
@Sebas, I don't really understand why the visibility of the children matters here. Anyway, if the answer wasn't useful for you particular case (whatever it might be) but points to a solution (or explanation why there isn't a solution) for most programmers, it's still a valid answer in my humble opinion. If you like better one of the other answers (especially one that was made at a later time, when technology improved and new options became available), I believe that a request to change the correct answer would be more appropriate than a downvote. In any case, thanks for the heads-up :)Expect
@figha, not the children, but if the element you're watching is in a frame that is displayed=none, you won't have any sign of it.Will
Similar to the issue mentioned by @Will is, consider you have descendent elements under the element becoming visible that want to listen for visibility. Even with well organized code you would have to iterate through every descendent element to dispatch an event. And putting the code for them directly where the visibility change is effected is much too coupled.Coray
I'm looking for the answer to the initial question as well. In my case I'm trying to track a 3rd party pop up so I'm not the one triggering the event becoming visible. This answer wasn't helpful.Caruso
V
149

Going forward, the new HTML Intersection Observer API is the thing you're looking for. It allows you to configure a callback that is called whenever one element, called the target, intersects either the device viewport or a specified element. It's available in latest versions of Chrome, Firefox and Edge. See https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API for more info.

Simple code example for observing display:none switching:

// Start observing visbility of element. On change, the
//   the callback is called with Boolean visibility as
//   argument:

function respondToVisibility(element, callback) {
  var options = {
    root: document.documentElement,
  };

  var observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      callback(entry.intersectionRatio > 0);
    });
  }, options);

  observer.observe(element);
}

In action: https://jsfiddle.net/elmarj/u35tez5n/5/

Viscacha answered 21/6, 2017 at 8:34 Comment(17)
> The Intersection Observer API allows you to configure a callback that is called whenever one element, called the target, intersects either the device viewport or a specified element;Robey
Note that the InteractionObserver does not work on Safari at the moment. caniuse.com/#feat=intersectionobserver It looks like there is a patch in progress. bugs.webkit.org/show_bug.cgi?id=159475Jeanicejeanie
Until Safari fixes its webkit you can use the solution from @user3588429 reliablyJeanicejeanie
When is IE's lack of support for anything cool like this going to stop ruining our lives?Beker
@PeterMoore as soon as you stop trying to support it. Just don't.Street
LOL. Sometimes it's not our decision.Beker
If, like me, you were hoping to find a brief check of whether an element is visible within scrolled area, see #5354434. If you want a callback when the element is on screen, no answer will deign to tell you to connect that hook to a scrollbar handler.Weatherspoon
Exactly what I needed!Unloose
As far as I can tell the intersection observer does not trigger the callback when a parent elements style is changed from none to block which causes the child (observed) to become visibile. Otherwise this would be a perfect solution for me.Delve
@DustinPoissant: maybe I don't understand the issue you're having, but in my testing it does. Have you tried the fiddle above? I adjusted it to exactly reflect your case (as I understand it) here: jsfiddle.net/y7n35jah/13 . Does this work for you? Or do I misunderstand?Viscacha
@ElmarJansen Every small example i try and set up in pens and such it does work properly. But in the productions websites it does not appear to be working. I had to find another solution other than IntersectionObserver, but i wish it would work. I can not disclose the sites here unfortunately, my employer has over 10,000 websites in the automotive sector with billions of daily users so unfortunately i cant not just post the code here for help. But I was thinking it had something to do with the fade in (opacity) that cause it to not work, but as I said we found another non IO solution.Delve
@ElmarJansen very useful piece of code!! Thanks for sharing. Something was resetting the visible state of one of my elements and could not find out what it was until I used the avbove!Hallee
@DustinPoissant: as far as I can tell, @ElmarJansen is right: IntersectionOberver reports changes of visibility, even if determined by the visibility of a parent, and even if this change is the result of a media query.Epidemiology
It returned true for all the element I added to observation list even though only first in list is actually visibleClerk
Is this still the best way to do it in 2021? Are there alternatives to this solution for Angular (v12)?Tailpiece
This doesn't work for elements that are visible outside of viewport. I don't know why this is best voted answer so far, as it's actually triggers just for current viewport elements..Replacement
@Kos: maybe I misunderstand your point, but in my testing, this works for elements that are not visible in the viewport. Note that this code sets the "root" option to override the default target (which would indeed be the viewport if not overridden) and observe the root element of the document instead.Viscacha
G
81
var targetNode = document.getElementById('elementId');
var observer = new MutationObserver(function(){
    if(targetNode.style.display != 'none'){
        // doSomething
    }
});
observer.observe(targetNode, { attributes: true, childList: true });

I might be a little late, but you could just use the MutationObserver to observe any changes on the desired element. If any change occurs, you'll just have to check if the element is displayed.

Globose answered 17/1, 2018 at 14:9 Comment(6)
This does not work when the targetNode is not displayed because an ancestor is not displayed but then gets displayed.Intermeddle
Upvoted! I used this in another stackoverflow answer: #48792745Affinal
Unfortunately, this only works when using inline styles on the element, not when the change is a result of change of CSS-class (which is the more likely scenario, given that in the former situation you probably already have full programatic control). Fiddle that shows the issue: jsfiddle.net/elmarj/uye62Lxc/4. See here for a more complete discussion of how to observe style-changes: dev.to/oleggromov/observing-style-changes---d4fViscacha
Actually the correct Observer to use here would be an IntersectionObserver - https://mcmap.net/q/94577/-detecting-that-user-has-scrolled-to-the-bottom-of-the-div-without-scroll-eventAbbotsen
This solution is useful for when you want to toggle an element on and off, and have no direct control of that element, and want to react to the display value changeLifton
Modified it to height change and worked great for my case. TnxEnterpriser
A
45

If you just want to run some code when an element becomes visible in the viewport:

function onVisible(element, callback) {
  new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      if(entry.intersectionRatio > 0) {
        callback(element);
        observer.disconnect();
      }
    });
  }).observe(element);
  if(!callback) return new Promise(r => callback=r);
}

When the element has become visible (event slightly) the intersection observer calls callback and then destroys itself with .disconnect().

Use it like this:

onVisible(document.querySelector("#myElement"), () => console.log("it's visible"));

or like this:

await onVisible(document.querySelector("#myElement"));
console.log("it's visible");

If you want the callback to trigger when the element becomes fully visible then you should change entry.intersectionRatio > 0 to entry.intersectionRatio === 1.

If the element is already visible when you call onVisible, then the callback will fire immediately.

Related: If you want to immediately get a true or false for whether the element is currently visible, use this code (change the intersectionRatio to suit your requirements).

Adalard answered 26/2, 2021 at 23:39 Comment(2)
This should be the accepted answer.Borras
Perfect! Should be Top answerDouala
B
17

There is at least one way, but it's not a very good one. You could just poll the element for changes like this:

var previous_style,
    poll = window.setInterval(function()
{
    var current_style = document.getElementById('target').style.display;
    if (previous_style != current_style) {
        alert('style changed');
        window.clearInterval(poll);
    } else {
        previous_style = current_style;
    }
}, 100);

The DOM standard also specifies mutation events, but I've never had the chance to use them, and I'm not sure how well they're supported. You'd use them like this:

target.addEventListener('DOMAttrModified', function()
{
    if (e.attrName == 'style') {
        alert('style changed');
    }
}, false);

This code is off the top of my head, so I'm not sure if it'd work.

The best and easiest solution would be to have a callback in the function displaying your target.

Baese answered 22/9, 2009 at 20:3 Comment(2)
Interesting, thank you. A few years later, the status of DOMAttrModified has changed: "Deprecated. This feature has been removed from the Web standards. Though some browsers may still support it, it is in the process of being dropped." (Mozilla)Dullard
MutationObserver is the new new.Apis
D
15

I had this same problem and created a jQuery plugin to solve it for our site.

https://github.com/shaunbowe/jquery.visibilityChanged

Here is how you would use it based on your example:

$('#contentDiv').visibilityChanged(function(element, visible) {
    alert("do something");
});
Disenthral answered 28/4, 2015 at 19:0 Comment(1)
This solution uses .is(':visible') along with setTimeout.Outburst
H
10

As @figha says, if this is your own web page, you should just run whatever you need to run after you make the element visible.

However, for the purposes of answering the question (and anybody making Chrome or Firefox Extensions, where this is a common use case), Mutation Summary and Mutation Observer will allow DOM changes to trigger events.

For example, triggering an event for a elements with data-widget attribute being added to the DOM. Borrowing this excellent example from David Walsh's blog:

var observer = new MutationObserver(function(mutations) {
    // For the sake of...observation...let's output the mutation to console to see how this all works
    mutations.forEach(function(mutation) {
        console.log(mutation.type);
    });    
});

// Notify me of everything!
var observerConfig = {
    attributes: true, 
    childList: true, 
    characterData: true 
};

// Node, config
// In this case we'll listen to all changes to body and child nodes
var targetNode = document.body;
observer.observe(targetNode, observerConfig);

Responses include added, removed, valueChanged and more. valueChanged includes all attributes, including display etc.

Hook answered 3/7, 2014 at 17:49 Comment(4)
Probably because he wants to know when it is actually on screen. I do, I can only assume the downvoter has the same reason.Favorable
Doesn't answer the question, and posting only links isn't advised.Luteal
@AlexHolsgrove I've added an example, from the link. Since the poster talks about adding a toolbar to the page, it seems like they might be adding a toolbar to a third party page and waiting for an element to be added to the DOM, in which case Mutations is the best API.Hook
Note that this still does not cover that part that explains where in the mutation observation process "visibility" comes in.Vaughn
M
6

A simple solution to this which works even for nested elements is to use the ResizeObserver.

It should work in all modern browsers (https://developer.mozilla.org/en-US/docs/Web/API/Resize_Observer_API).

When an element has css rule display: none applied to it (whether directly or via an ancestor element) then all of its dimensions will be zero. So in order to detect becoming visible we just need an element with non-zero dimensions when visible.

const element = document.querySelector("#element");
const resizeWatcher = new ResizeObserver(entries => {
  for (const entry of entries) {
    console.log("Element", entry.target, 
      (entry.contentRect.width === 0) ? 
      "is now hidden" : 
      "is now visible"
    )
  }
});

resizeWatcher.observe(element)
Miasma answered 18/11, 2021 at 12:2 Comment(1)
This is best because waiting for an element to intersect with the viewport means you can't style until the user scrolls to it. Sounds like nothing major, but there's a difference between styling while the page is scrolling versus styling the moment the element is sized. Using ResizeObserver works around the issue of Web Components being defined out of order. Sometimes the element isn't sized yet when connectedCallback is fired.Polypeptide
E
3

Just to comment on the DOMAttrModified event listener browser support:

Cross-browser support

These events are not implemented consistently across different browsers, for example:

  • IE prior to version 9 didn't support the mutation events at all and does not implement some of them correctly in version 9 (for example, DOMNodeInserted)

  • WebKit doesn't support DOMAttrModified (see webkit bug 8191 and the workaround)

  • "mutation name events", i.e. DOMElementNameChanged and DOMAttributeNameChanged are not supported in Firefox (as of version 11), and probably in other browsers as well.

Source: https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Mutation_events

Entangle answered 26/11, 2014 at 17:9 Comment(1)
Use MutationObserver instead. Mutation Events are deprecated. #20421077Fulgurate
J
1

Expanding on Elmar's earlier answer, I used this to put focus on an input box in a Bootstrap navbar submenu.

I wanted the focus to go on the search box when the menu was expanded. .onfocus() wasn't working, I think because the element isn't visible at the time the event is triggered (even with the mouseup event). This worked perfectly though:

<ul class="navbar-nav ms-auto me-0 ps-3 ps-md-0">
    <li class="nav-item dropdown">
        <a class="nav-link dropdown-toggle" title="Search" id="navbardrop" data-bs-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
            <i class="fas fa-search"></i>
        </a>
        <div class="dropdown-menu dropdown-menu-end search-menu">
            <form action="{% url 'search' %}" method="get">
                <div class="form-group row g-1 my-1 pb-1">
                    <div class="col">
                        <input type="text" name="query" id="searchbox" class="form-control py-1 ps-2" value="{% if search_query %}{{ search_query }}{% endif %}">
                    </div>
                    <div class="col-auto">
                        <input type="submit" value="Search" class="btn-primary form-control py-1">
                    </div>
                </div>
            </form>
        </div>
    </li>
</ul>

Then in the js:

respondToVisibility = function (element, callback) {
  var options = {
    root: document.documentElement,
  };

  var observer = new IntersectionObserver((entries, observer) => {
    entries.forEach((entry) => {
      callback(entry.intersectionRatio > 0);
    });
  }, options);

  observer.observe(element);
};

respondToVisibility(document.getElementById("searchbox"), (visible) => {
  if (visible) {
    document.getElementById("searchbox").focus();
  }
});
Judon answered 3/8, 2021 at 12:49 Comment(0)
E
-1

Javascript events deal with User Interaction, if your code is organised enough you should be able to call the initialising function in the same place where the visibility changes (i.e. you shouldn't change myElement.style.display on many places, instead, call a function/method that does this and anything else you might want).

Expect answered 22/9, 2009 at 20:47 Comment(7)
But how would you be able to detect the change in visibility (i. e., call a function as soon as an element becomes visible)?Sev
I thiiiink you can't do that. What you would do instead of trying to detect the change, is knowing exactly where it is provoked, and put your call there. If the change in visibility is not something your own code is directly responsible for (e.g. some lib) and the logic by which that happens is very contrived, I guess you're out of luck? :)Expect
I downvoted this question because despite all the sense it makes, it's irrelevant to the actual issue. (if I display none on a frame, all children elements become invisible)Will
@Sebas, I don't really understand why the visibility of the children matters here. Anyway, if the answer wasn't useful for you particular case (whatever it might be) but points to a solution (or explanation why there isn't a solution) for most programmers, it's still a valid answer in my humble opinion. If you like better one of the other answers (especially one that was made at a later time, when technology improved and new options became available), I believe that a request to change the correct answer would be more appropriate than a downvote. In any case, thanks for the heads-up :)Expect
@figha, not the children, but if the element you're watching is in a frame that is displayed=none, you won't have any sign of it.Will
Similar to the issue mentioned by @Will is, consider you have descendent elements under the element becoming visible that want to listen for visibility. Even with well organized code you would have to iterate through every descendent element to dispatch an event. And putting the code for them directly where the visibility change is effected is much too coupled.Coray
I'm looking for the answer to the initial question as well. In my case I'm trying to track a 3rd party pop up so I'm not the one triggering the event becoming visible. This answer wasn't helpful.Caruso
M
-5

my solution:

; (function ($) {
$.each([ "toggle", "show", "hide" ], function( i, name ) {
    var cssFn = $.fn[ name ];
    $.fn[ name ] = function( speed, easing, callback ) {
        if(speed == null || typeof speed === "boolean"){
            var ret=cssFn.apply( this, arguments )
            $.fn.triggerVisibleEvent.apply(this,arguments)
            return ret
        }else{
            var that=this
            var new_callback=function(){
                callback.call(this)
                $.fn.triggerVisibleEvent.apply(that,arguments)
            }
            var ret=this.animate( genFx( name, true ), speed, easing, new_callback )
            return ret
        }
    };
});

$.fn.triggerVisibleEvent=function(){
    this.each(function(){
        if($(this).is(':visible')){
            $(this).trigger('visible')
            $(this).find('[data-trigger-visible-event]').triggerVisibleEvent()
        }
    })
}
})(jQuery);

for example:

if(!$info_center.is(':visible')){
    $info_center.attr('data-trigger-visible-event','true').one('visible',processMoreLessButton)
}else{
    processMoreLessButton()
}

function processMoreLessButton(){
//some logic
}
Misgive answered 24/4, 2014 at 6:14 Comment(3)
You should tell everyone that you've used the original jQuery's code, it would inspire more confidence ;)Nepheline
"my solution". The nerve on this guy.Royston
@AndréSilva Maybe he wrote jQuery. :OLackaday

© 2022 - 2025 — McMap. All rights reserved.