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
load
at thelink
element"
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
setTimeout
and check for changes indocument.styleSheets
setTimeout
and 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
readystatechange
andload
(tested years ago, too lazy to test now again). Now with IE9 maybeaddEventListener
will work too? - Firefox (like before) fires nothing. It updates the
length
ofdocument.styleSheets
immediately, 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
load
viaonload
and viaaddEventListener
too. Like FF it also incrementsdocument.styleSheets.length
immé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.styleSheets
only 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.length
trick 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
load
on stylesheets 🙂 No doubt about it. IMO all browsers should fireload
on 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
style
element, not alink
- add
@import "URL"
- poll for access to that style node's
cssRules
collection
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