20

I'm trying to attach an event handler to the load event of a link tag, to execute some code after a stylesheet has loaded.

new_element = document.createElement('link');
new_element.type = 'text/css';
new_element.rel = 'stylesheet';
new_element.href = 'http://domain.tld/file.css';
new_element.addEventListener('load', function() { alert('foo'); }, false);
document.getElementsByTagName('head')[0].appendChild(new_element)

I have tried onreadystatechange as well:

new_element.onreadystatechange = function() { alert('foo'); }

Unfortunately neither approach results in an alert being triggered. Furthermore, new_element.onload is null after registering a handler for the load event with addEventListener. Is that normal?

PS: I may not use any framework in solving this.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
pgn
  • 669
  • 1
  • 6
  • 16
  • I'd love it if you revisited your decision on which answer you picked, the one I left below is event based and should be way better for you ;) – csuwldcat Nov 18 '11 at 16:09
  • hey, you ever consider check-marking the better answer with 36 votes, instead of the kludgy try/catch hack with 8? – csuwldcat Sep 18 '14 at 18:24
  • 2
    All major browsers support `onload` for `` elements now, according to https://pie.gd/test/script-link-events/ – Nickolay Aug 23 '19 at 23:15
  • @Nickolay: You should make your comment an answer, as it is now the right answer for all living browsers. – Sean McMillan Jan 05 '22 at 16:08

10 Answers10

53

Here's what is, in my opinion, a better solution for this issue that uses the IMG tag and its onerror event. This method will do the job without looping, doing contorted style observance, or loading files in iframes, etc. This solution fires correctly when the file is loads, and right away if the file is already cached (which is ironically better than how most DOM load events handle cached assets). Here's a post on my blog that explains the method - Back Alley Coder post - I just got tired of this not having a legit solution, enjoy!

var loadCSS = function(url, callback){
    var link = document.createElement('link');
        link.type = 'text/css';
        link.rel = 'stylesheet';
        link.href = url;
 
    document.getElementsByTagName('head')[0].appendChild(link);
 
    var load_detect = document.createElement('img');
    load_detect.onerror = function(){
        // error in image terms because of wrong MIME type,
        // but success in terms of content from the URL having downloaded
        if(callback) callback(link);
    }
    load_detect.src = url;
}
EoghanM
  • 25,161
  • 23
  • 90
  • 123
csuwldcat
  • 8,021
  • 2
  • 37
  • 32
  • seems pretty darn good to me... How well supported is this across browsers? – simon Mar 28 '11 at 15:19
  • 2
    You don't need this method for IE and Opera, they have supported link tag onload for a while now. Just use this in Gecko and Webkit based browsers. I am augmenting MooTools More's official Asset method to intelligently use this if onload is not supported on link elements, should be in one of our upcoming releases of MooTools. – csuwldcat Apr 02 '11 at 19:09
  • 1
    doesn't seem to work in Safari on a Mac, when I try it? (fine on Chrome and FF, though). Haven't tried it on Safari under Windows yet. – simon Apr 03 '11 at 09:59
  • I believe it was because I was appending the img element to the HTML element instead of the body element. It works for me now in Safari. – csuwldcat May 19 '11 at 21:01
  • @simon, you're right, `load` event doesn't work in Safari under Windows too. I have such an expression `link.addEventListener('load', func, false)`. And `load` event fires `func` in Chrome and FF, but doesn't fire in Safari. – Green Jan 20 '13 at 21:44
  • @Green I have fixed the code since simon's comment, it had nothing to do with addEventListener - of course link.addEventListener('load', func, false) doesn't work, it wont work in a lot of places, that's the whole point! Please take off the downvote you just applied, the method works, and you are showing an example of something that has nothing to do with how this answer operates/works. – csuwldcat Jan 24 '13 at 16:32
  • @csuwldcat, it's some mistake, I didn't downvote your answer. I didn't upvote then too but I've just done it. Ok? It was 19 when I read your comment, my one is 20th – Green Jan 24 '13 at 17:42
  • @Green oh, I didn't realize, sorry. They seemed to happen at the same time, my bad! – csuwldcat Jan 24 '13 at 22:05
  • You can use `new Image` instead of `document.createElement`. It's shorter and makes more sense (to me at least). – rvighne Jan 28 '14 at 05:09
  • This is way better than the other solution. Kudos. – Etai Sep 17 '14 at 06:01
  • So how do I detect whether I need to use this solution or not? I would rather use link onload if it is supported, and only fall back to this if necessary. – Michael Sep 26 '14 at 15:52
  • This didn't work for me, probably because the problem has now shifted from CSS to the font itself. – Nicu Surdu Apr 18 '15 at 20:28
  • 1
    This is oddly working on WebKit targeting back to 2011. Bravo. – deepelement Aug 12 '15 at 15:07
  • There is [a very elaborate article](https://blogs.msdn.microsoft.com/ieinternals/2012/05/05/use-img-tags-only-for-images/) why abusing images for preloading is bad. Now, this is not a preload situation since the `` is included first. But what if you try to load a really large CSS and the `` aborts and throws the error before the CSS got loaded entirely because it recognized this couldn't possible be an image (as pointed out in the article) ? That would leave you with the callback being fired prematurely after all. – Daniel A. R. Werner Apr 22 '16 at 18:02
11

For CSS stylesheets (not LINK elements in general) i'm using manual interval, by poking it's rules length. It works crossbrowser (AFAIT).

try {
  if ( cssStylesheet.sheet && cssStylesheet.sheet.cssRules.length > 0 )
    cssLoaded = 1;
  else if ( cssStylesheet.styleSheet && cssStylesheet.styleSheet.cssText.length > 0 )
    cssLoaded = 1;
  else if ( cssStylesheet.innerHTML && cssStylesheet.innerHTML.length > 0 )
    cssLoaded = 1;
}
catch(ex){}

In code above, the cssStylesheet is DOMElement.

Tobias Cudnik
  • 9,240
  • 4
  • 24
  • 18
  • That works great Tobiasz! Thanks! It took me a little bit to understand what you were talking about so for those of you who might be a little confused like me, check out my answer. – Matt Jan 17 '11 at 05:39
  • 4
    This is fine, except if you load css cross-domain. If your CSS is on a CDN or something, cssRules is not available due to cross-domain restrictions. See also http://www.phpied.com/when-is-a-stylesheet-really-loaded/ – Sean McMillan Dec 06 '12 at 19:45
7

Even if you add an inline:

<link rel="stylesheet" type="text/css" href="foo.css" onload="alert('xxx')"/>

It won't fire in FireFox as there isn't an onload event for link elements. (It will work in IE)

Alex K.
  • 171,639
  • 30
  • 264
  • 288
  • 1
    As of Firefox 9, link tag onload and onerror events are supported. The only browsers that fails to provide these crucial events are Google Chrome and Apple Safari. – csuwldcat Jan 06 '12 at 07:23
  • 1
    Chrome/Safari have implemented onload/onerror on `` since. IE doesn't implement `onerror` according to this awesome compatibility table: https://pie.gd/test/script-link-events/ – Nickolay Aug 23 '19 at 23:13
3

This function has held up on all browsers as well as in both cross domain and same domain situations, also this handles the injection of javascripts as well as stylesheets.

function loadScript(src, type, callback_fn) {
    var loaded = false, scrpt, img;
    if(type === 'script') {
        scrpt = document.createElement('script');
        scrpt.setAttribute('type', 'text/javascript')
        scrpt.setAttribute('src', src);

    } else if(type === 'css') {
        scrpt = document.createElement('link')
        scrpt.setAttribute('rel', 'stylesheet')
        scrpt.setAttribute('type', 'text/css')
        scrpt.setAttribute('href', src);
    }
    document.getElementsByTagName('head')[0].appendChild(scrpt);

    scrpt.onreadystatechange = function(){
        if (this.readyState === 'complete' || this.readyState === 'loaded') {
            if(loaded === false) {
                callback_fn();
            }
            loaded = true;
        }
    };

    scrpt.onload = function() {
        if(loaded === false) {
            callback_fn();
        }
        loaded = true;
    };

    img = document.createElement('img');
    img.onerror = function(){
        if(loaded === false) {
            callback_fn();
        }
        loaded = true;
    }
    img.src = src;
};
Tyler Biscoe
  • 2,322
  • 3
  • 20
  • 33
3

All credit goes to Tobiasz up above, but here's a little expansion on what he said:

function _cssIsLoaded(cssStylesheet) {
    var cssLoaded = 0;
    try {
        if ( cssStylesheet.sheet && cssStylesheet.sheet.cssRules.length > 0 )
            cssLoaded = 1;
        else if ( cssStylesheet.styleSheet && cssStylesheet.styleSheet.cssText.length > 0 )
            cssLoaded = 1;
        else if ( cssStylesheet.innerHTML && cssStylesheet.innerHTML.length > 0 )
            cssLoaded = 1;
        }
        catch(ex){ }

        if(cssLoaded) {
            // your css is loaded! Do work!
            // I'd recommend having listeners subscribe to cssLoaded event, 
            // and then here you can emit the event (ie. EventManager.emit('cssLoaded');
        } else {
            // I'm using underscore library, but setTimeout would work too
            // You basically just need to call the function again in say, 50 ms
            _.delay(_.bind(this._cssIsLoaded, this), 50, cssStylesheet);
        }
}

You'd call it with something like (using jquery):

var link = $("<link>");
link.attr({
    type: 'text/css',
    rel: 'stylesheet',
    href: sheet
});

$("head").append(link);
// link.get(0), because you want the actual element, not jQuery-wrapped element
self._cssIsLoaded(link.get(0));
Matt
  • 22,224
  • 25
  • 80
  • 116
3

The link.innerHTML, link.styleSheet and cssRules are all good approaches, but they do not work for stylesheets that belong to a domain outside of the origin domain (so cross-site stylesheet loading fails). This can be pretty unexpected when a subdomain vs a domain is used (www for example) or a static CNS is used. And its pretty annoying since elements have no same-origin restriction.

Here's a solution that that uses the onload method for browsers that support it (IE and Opera), but then uses a timed interval for browsers that do not and compares the ownerNode and owningElement nodes to check to see when the stylesheet has made its way into the DOM.

http://www.yearofmoo.com/2011/03/cross-browser-stylesheet-preloading.html

matsko
  • 21,895
  • 21
  • 102
  • 144
  • Link is 404, but available via wayback: http://wayback.archive.org/web/20160809031346/http://www.yearofmoo.com/2011/03/cross-browser-stylesheet-preloading.html – Sean McMillan Jan 16 '17 at 19:00
2

Nice shot Matt. I've created this helper with your comment.

var CSSload = function(link, callback) {
    var cssLoaded = false;
    try{
        if ( link.sheet && link.sheet.cssRules.length > 0 ){
            cssLoaded = true;
        }else if ( link.styleSheet && link.styleSheet.cssText.length > 0 ){
            cssLoaded = true;
        }else if ( link.innerHTML && link.innerHTML.length > 0 ){
            cssLoaded = true;
        }
    }
    catch(ex){ }
    if ( cssLoaded ){
        callback();
    }else{
        setTimeout(function(){
            CSSload(link);
        }, 100);
    }
};

Usage:

var $link = $('<link rel="stylesheet" media="screen" href="file.css"/>');
$('head').append($link);
CSSload($link.get(0), function(){
  // do something onLoad
});
Tom Roggero
  • 5,777
  • 1
  • 32
  • 39
0

E.g. Android browser doesn't support "onload" / "onreadystatechange" events for element: http://pieisgood.org/test/script-link-events/
But it returns:

"onload" in link === true

So, my solution is to detect Android browser from userAgent and then wait for some special css rule in your stylesheet (e.g., reset for "body" margins).
If it's not Android browser and it supports "onload" event- we will use it:

var userAgent = navigator.userAgent,
    iChromeBrowser = /CriOS|Chrome/.test(userAgent),
    isAndroidBrowser = /Mozilla\/5.0/.test(userAgent) && /Android/.test(userAgent) && /AppleWebKit/.test(userAgent) && !iChromeBrowser; 

addCssLink('PATH/NAME.css', function(){
    console.log('css is loaded');
});

function addCssLink(href, onload) {
    var css = document.createElement("link");
    css.setAttribute("rel", "stylesheet");
    css.setAttribute("type", "text/css");
    css.setAttribute("href", href);
    document.head.appendChild(css);
    if (onload) {
        if (isAndroidBrowser || !("onload" in css)) {
            waitForCss({
                success: onload
            });
        } else {
            css.onload = onload;
        }
    }
}

// We will check for css reset for "body" element- if success-> than css is loaded
function waitForCss(params) {
    var maxWaitTime = 1000,
        stepTime = 50,
        alreadyWaitedTime = 0;

    function nextStep() {
        var startTime = +new Date(),
            endTime;

        setTimeout(function () {
            endTime = +new Date();
            alreadyWaitedTime += (endTime - startTime);
            if (alreadyWaitedTime >= maxWaitTime) {
                params.fail && params.fail();
            } else {
                // check for style- if no- revoke timer
                if (window.getComputedStyle(document.body).marginTop === '0px') {
                    params.success();
                } else {
                    nextStep();
                }
            }
        }, stepTime);
    }

    nextStep();
}

Demo: http://codepen.io/malyw/pen/AuCtH

Serg Hospodarets
  • 4,658
  • 4
  • 25
  • 31
0

This method suits me better.

// Create a <link> and set it in an HTML node
const 
$link = document.createElement("link");
$link.setAttribute('rel', 'stylesheet');
$link.setAttribute('href', '/build/style/test.css');
document.head.appendChild($link);

// Event about load
onloadCSS($link).then(function(url) {
    console.log('CSS loaded: ' + url);
}).catch(function(e) {
    console.log('Failed to load CSS');
});

function onloadCSS($link){
    return new Promise(function(resolve, reject) {
        const 
        options = {
            headers: ({'Accept': 'text/css,*/*;q=0.1'})
        }, 
        url = $link.getAttribute('href');

        fetch(url, options).then(function(response) {
            if(response.status === 200){
                return resolve(url);
            }

            return reject({status: response.status});
        }).catch(function(e) {
            return reject(e);
        });      
    });
}
-4

An href does not have a load event you need to apply your stuff as the page itself loads e.g.

window.onload = function(){
//code here

}

use the function on this page: http://www.dustindiaz.com/top-ten-javascript/

to add the event the body element (in line would be )

matpol
  • 3,042
  • 1
  • 15
  • 18
  • Thanks- are you saying tossing a stylesheet link tag in the header will cause window.onload to fire after the stylesheet loads ? i've tried this just right now and it did not. Also I'm already running from a window.onload handler. What i'm trying to do is, after the page has loaded (window.onload) load a css (createElement('link')) , after which load some javascript (createElement('script')) The bottom line is, the script that is loaded last, creates nodes that need some css classes to be already loaded – pgn Apr 14 '10 at 08:38
  • 1
    the javascript loads/runs once the whole page has completed loading so you can only modify the dom once this has happened so you need to run any scripts using the onload event handler which can only be attached to the body element. So all you code needs run in the onload event handler. – matpol Apr 14 '10 at 08:55