Often we want to load a CSS file on-demand by inserting a link node. And we want to know when the file finished loading in order to call a callback function for example.
Long story short: turns out this is harder than it should be and really unnecessary hard in Firefox. I hereby beg on behalf of many frustrated developers: please, Firefox 4, please fire a load event when a stylesheet loads.
If you think this is "a change we can believe in", tell the browser vendors to fire a
load event on link elements as the HTML5 standard mandates. Here's the list of bugs for each browser to comment on and point people to:
Here's what the standard says:
"Once the attempts to obtain the resource and its critical subresources are complete, the user agent must, if the loads were successful, queue a task to fire a simple event named
loadat thelinkelement"
With that out of the way, let's see what we have here.
// my callback function // which relies on CSS being loaded function CSSDone() { alert('zOMG, CSS is done'); } // load me some stylesheet var url = "http://tools.w3clubs.com/pagr/1.sleep-1.css", head = document.getElementsByTagName('head')[0]; link = document.createElement('link'); link.type = "text/css"; link.rel = "stylesheet" link.href = url; // MAGIC // call CSSDone() when CSS arrives head.appendChild(link);
Options for the magic part, sorted from nice-and-easy to ridiculous
- listen to
link.onload - listen to
link.addEventListener('load') - listen to
link.onreadystatechange setTimeoutand check for changes indocument.styleSheetssetTimeoutand check for changes in the styling of a specific element you create but style with the new CSS
5th option is too crazy and assumes you have control over the content of the CSS, so forget it. Plus it checks for current styles in a timeout meaning it will flush the reflow queue and can be potentially slow. The slower the CSS to arrive, the more reflows. So, really, forget it.
So how about implementing the magic?
// MAGIC // #1 link.onload = function () { CSSDone('onload listener'); } // #2 if (link.addEventListener) { link.addEventListener('load', function() { CSSDone("DOM's load event"); }, false); } // #3 link.onreadystatechange = function() { var state = link.readyState; if (state === 'loaded' || state === 'complete') { link.onreadystatechange = null; CSSDone("onreadystatechange"); } }; // #4 var cssnum = document.styleSheets.length; var ti = setInterval(function() { if (document.styleSheets.length > cssnum) { // needs more work when you load a bunch of CSS files quickly // e.g. loop from cssnum to the new length, looking // for the document.styleSheets[n].href === url // ... // FF changes the length prematurely :() CSSDone('listening to styleSheets.length change'); clearInterval(ti); } }, 10); // MAGIC ends
Test
Test page - riiiight here. I'm loading a CSS file delayed two seconds on the server. Attaching all those event listeners and timeouts above. Adding another timeout that just says "... and two seconds later ... " after (you guessed it!) two seconds. Now observing los resultados...
Results
- IE fires
readystatechangeandload(tested years ago, too lazy to test now again). Now with IE9 maybeaddEventListenerwill work too? - Firefox (like before) fires nothing. It updates the
lengthofdocument.styleSheetsimmediately, not waiting for the file to actually arrive. So the outcome in my test log is:zOMG, CSS #1 is done: listening to styleSheets.length change ... and two seconds later ...
- Opera fires
loadviaonloadand viaaddEventListenertoo. Like FF it also incrementsdocument.styleSheets.lengthimmédiatement. The outcome:zOMG, CSS #1 is done: listening to styleSheets.length change ... and two seconds later ... zOMG, CSS #1 is done: onload listener zOMG, CSS #1 is done: DOM's load event
- Chrome and Safari will not fire events but will update
document.styleSheetsonly when the file arrives, yey!... and two seconds later ... zOMG, CSS #1 is done: listening to styleSheets.length change
All in all, there's at least one way to tell when the stylesheet is loaded in each browser, except Firefox. Now that's embarrassing.
Is there really no hope for Firefox?
If you go really crazy than yes - implement magic #5 but it has serious drawbacks.
Otherwise the object trick should do - all browsers seem to fire load and/or readystatechange event consistently. Obviously it's a little more complicated. Although probably not as complicated as monitoring the document.styleSheets collection
I also tried MozAfterPaint - it might work but didn't for me, because my CSS didn't change anything on the page that required repaint. Obviously not a fit-all solution.
Another thing that failed was checking document.styleSheets[n].cssRules. Although document.styleSheets.length is updated immediately, I was thinking FF cannot update document.styleSheets[n].cssRules (document.styleSheets[n].sheet.cssRules) until the CSS actually arrives. However since FF 3.5 (or thereabouts) you don't have access to cssRules collection when the file is hosted on a different domain (CDN say). Security thing, you see. Even cssRules.length would've been enough, but nah.
Takeaways
- Any ideas about a clever (or not so clever) workaround that lets us figure out when CSS is loaded in FF? Please comment.
- Libraries can benefit from
document.styleSheets.lengthtrick to support Chrome and Safari. I know at least that YUI3 doesn't support callbacks onY.Get.css()in Safari (nor FF) - Firefox 4 must implement
loadon stylesheets 🙂 No doubt about it. IMO all browsers should fireloadon everything related to external resources.
UPDATE: FF solved!
Thanks to Ryan's comment below, Zach and Oleg, turns out there is something that works. And that is:
- you create a
styleelement, not alink - add
@import "URL" - poll for access to that style node's
cssRulescollection
It just happens so that Firefox will not populate this collection until the file arrives!
var style = document.createElement('style'); style.textContent = '@import "' + url + '"'; var fi = setInterval(function() { try { style.sheet.cssRules; // <--- MAGIC: only populated when file is loaded CSSDone('listening to @import-ed cssRules'); clearInterval(fi); } catch (e){} }, 10); head.appendChild(style);
Updated my test and seems to work just fine.
Whew!
I still maintain that this is unnecessarily complex and all browsers should simply fire load event. Seems to me this FF behavior might change at any time, as well as Safari's not populating document.styleSheet
One thing to note: this access to cssRules is not a failure of the same security check that doesn't allow cssRules access with outside domains. In this case we're accessing cssRules of the inline style and that's fine. We still cant access the styles of the @import-ed file from a different domain.
Side note: makes me wonder - this could be a technique to preload JS without executing too 🙂
Comments? Find me on BlueSky, Mastodon, LinkedIn, Threads, Twitter




