When is a stylesheet really loaded?
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 fire loadon 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
This entry was posted on Thursday, March 17th, 2011 and is filed under browsers, CSS, performance. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.
Get notification for future posts: follow me on Twitter or subscribe to my RSS feed

March 17th, 2011 at 11:40 pm
Would it much simpler by:
1) always having a “beacon” css rule in loaded CSS;
2) have this rule to apply to a hidden “beacon” element in page?
3) setInterval to check whether the “beacon” css rule has applied to be hidden “beacon”.
March 17th, 2011 at 11:48 pm
yes, that’s my magic #5
it has serious drawbacks though (as described above)
March 18th, 2011 at 12:06 am
Zach Leatherman and Oleg Slobodskoi both independently found a reliable way of detecting CSS load completion in Firefox. Zach wrote about the technique on his blog, and I recently implemented it in LazyLoad. It works well.
March 18th, 2011 at 12:21 am
Yah, I always was frustrated with the absence of “load” for s some time ago. Think, the best approach for “dynamic css” will be to embed the css into JS (in some static pre-processsing step) – and then embed the CSS after JS load.
March 18th, 2011 at 12:48 am
For firefox
create csslink -> loading xxx.css -> change some iframe style(change height or width) -> listen iframe resize event
when resize event be fired, the css file loaded.
March 18th, 2011 at 12:56 am
Thankyou, thankyou, thankyou Ryan
The test updated: http://www.phpied.com/files/cssonload/test.html
Updating the post now…
But still, FF should fire a load event. This behavior might break at any time,
March 18th, 2011 at 1:23 am
Here is the link for the Firefox bug on the onload issue:
https://bugzilla.mozilla.org/show_bug.cgi?id=185236
March 18th, 2011 at 1:30 am
Thanks James, I couldn’t find this one, although I found one that said that this one exists
Maybe the best strategy should be:
a/ add to HTML5 standard
b/ hold browsers accountable
Better than “I might do this at some point” (comment #26)
March 18th, 2011 at 3:27 am
Thanks for the detailed write-up, Stoyan!
Did you investigate if
link.type = "text/css";is really needed? I’m asking since it’s optional in HTML, sincerel="stylesheet"already impliestype="text/css". In which browser(s) does the script break if you omit this?March 18th, 2011 at 8:41 am
Hey Stoyan,
Your solution for Chrome and Safari seems a bit brittle. Any number of libraries could be updating the stylesheet count. Also, the load event does NOT indicate that the stylesheet is being applied to the document, unfortunately. I think this is the most important “event” to be listening for, isn’t it? Opera, Chrome, and Firefox execute the load event well before the styles are applied to any elements in the document. Finally, there are complications when loading cross-domain stylesheets (off of a CDN, for instance).
Take a look at the isLinkReady function in the cssx/css! AMD loader plugin (for AMD loaders like RequireJS and curl.js): https://github.com/unscriptable/cssx/blob/master/src/cssx/css.js#L177
There’s an ugly Chrome sniff+hack in there that I am trying to find a way to remove. This routine works on both same-domain and cross-domain urls as well as most browsers (FF 2+, Safari 3+, IE6+, Chrome 9+).
I agree this is a crazy mess! Plz! More votes on those browser tickets, folks!
– John
March 18th, 2011 at 9:26 am
Have you tried monitoring cssRules (for FF) in combination with checking to see if the exception code is 1000 (stylesheet loaded in FF XD)?
https://github.com/unscriptable/cssx/blob/master/src/cssx/css.js#L205
March 18th, 2011 at 9:41 am
I had a similar issue, here’s my solution: http://thudjs.tumblr.com/post/637855087/stylesheet-onload-or-lack-thereof
March 18th, 2011 at 12:10 pm
@Christos, this approach fails FF’s security policy when you load CSS from a different domain, e.g. a CDN
@Kris, this is interesting – so being able to tell from the exception code whether:
- cssRules is not ready yet (file not loaded)
vs.
- it’s ready but you “can’t touch this” because of security policy
Is this what you have in mind?
@Mathias, @unscriptable – yes, my code leaves to be desired, it’s more of an exploration at this stage. You should see all the detailed code review feedback I got from @jdalton
@unscriptable, I see this exception 1000 in FF, very nice. I think you can replace your style sniffing in chrome with check for stylesheets.length. I’m not sure that means it hasn’t been applied, but even if not, it will be a very good approximation
March 18th, 2011 at 12:24 pm
The HTML5 spec http://www.w3.org/TR/html5/semantics.html#the-link-element
“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 the link element”
March 18th, 2011 at 12:56 pm
@Stoyan, where the stylesheets.length test might fail is in cases in which you want to use the computed style of an element. For instance:
div.style.height = div.scrollHeight + ‘px’;
If some styles are not already applied to the div (because Chrome fired the load event too soon), then the calculation won’t be correct because the computed value, scrollHeight, won’t be final, yet.
I’m going to try the stylesheets.length test later and report back.
March 18th, 2011 at 1:17 pm
@Stoyan I haven’t noticed FF (3.6 or 4) having any issues with my approach. :^)
March 18th, 2011 at 2:21 pm
What about loading CSS via XMLHTTPRequest, then populating a tag with the contents? Ugly, but you should have good control over loading.
March 19th, 2011 at 1:58 am
Dethe – cross-domain restrictions
Christos, I replaced your function with:
function loadStyleSheets() {
if ( i.length ) loadStyleSheet( ‘http://tools.w3clubs.com/pagr/’ + i.shift() + ‘.sleep-1.css’, onComplete );
}
and got in the console:
stylesheet http://tools.w3clubs.com/pagr/1.sleep-2.css load: failed.
stylesheet http://tools.w3clubs.com/pagr/2.sleep-2.css load: failed.
stylesheet http://tools.w3clubs.com/pagr/3.sleep-2.css load: failed.
…
Although I can see the stylesheets load just fine in the Net panel
March 19th, 2011 at 2:00 am
@Emil – thanks, cool!
March 21st, 2011 at 4:47 am
Good write-up!
I was wondering about a detail in the FF solution from the update:
It’s using a try/catch-block. I’ve read some argument that using exceptions like that is very slow, because it will unwind the whole execution stack etc etc (this was probably referring to some C dialect).
Is this of any concern in JS?
Would it be possible, and if so, faster, to use if-statements (for example)?
Just looking for a general opinion here, not a detailed rewrite of the code
March 21st, 2011 at 9:58 am
Stoyan, you wrote “Side note: makes me wonder – this could be a technique to preload JS without executing too”
How can this possibly improve your existing technique for preloading CSS/JS without execution?
http://www.phpied.com/preload-cssjavascript-without-execution/
March 21st, 2011 at 12:37 pm
Just FYI: the
loadevent for the<link>element is defined in HTML5:http://www.w3.org/TR/html5/semantics.html#the-link-element
With it in the spec, I think (hope) that it’s just a matter of time before Firefox implements it.
March 22nd, 2011 at 9:17 am
[...] When is a stylesheet really loaded? [...]
May 17th, 2011 at 10:15 pm
[...] When is a stylesheet really loaded? / Stoyan’s phpied.com. [...]
May 17th, 2011 at 10:47 pm
Nice write-up Stoyan!
Just to contribute with the “craziness” of Magic solutions, someone suggested the following:
- load the link css url normally (injection)
- right after that create an image (new Image()) pointing its src to the same css url above
- listen to image onerror event which once fired means the css file was loaded
But someone else pointed out some issues with such solution:
- the css file should be cached to avoid performance penalty by requesting it twice
- if onerror fires because of a network issue then you got a false positive
+1 vote on those browser tickets
June 13th, 2011 at 5:02 pm
I wish this article existed a year ago. Just rewrote a bunch of code to better handle dynamic CSS loading. Thanks for the clear explanation.
June 30th, 2011 at 2:10 pm
[...] When is a stylesheet really loaded? / Stoyan's phpied.com [...]
July 1st, 2011 at 6:17 pm
When I need load a Dynamic CSS, I put It in a JS and check the load state by callback.
css.js contet Example:
writeCSS(‘h1{color:red}\
h2{color:blue}\
h3{color:green}’);
loadScript(‘css.js’,function(){
content.init();
})
August 9th, 2011 at 1:22 pm
[...] the implementation of onload for <link> elements is notoriously inconsistent across browsers. Plenty of browser-specific hacks exist to check if a stylesheet has loaded in a specific browser, but their longevity as browsers [...]
August 16th, 2011 at 3:56 pm
function loadScript(src, callback) {
var head = document.getElementsByTagName(‘head’)[0],
script = document.createElement(‘link’);
done = false;
script.setAttribute(‘src’, src);
script.setAttribute(‘type’, ‘text/css’);
script.setAttribute(‘charset’, ‘utf-8′);
script.onload = script.onreadstatechange = function() {
if (!done && (!this.readyState || this.readyState == ‘loaded’ || this.readyState == ‘complete’)) {
done = true;
script.onload = script.onreadystatechange = null;
if (callback) {
callback();
}
}
}
head.insertBefore(script, head.firstChild);
}
loadScript(‘style.css’, function() { alert(‘style sheet loaded.’); });
August 16th, 2011 at 10:12 pm
it’s good to have a solution for this but I wonder who really needs to load CSS from external domains in real life and, once loaded, needs to be notified.
Also CSS may use @import as well and the fact the CSS has been loaded does not mean assets are ready ( related images, etc )
I am pretty sure I need coffee now but can anybody tell me when is necessary to solve this problem? Many thanks
August 20th, 2011 at 4:51 pm
Andrea, my friend, coffee FTW
You often need 3rd party domain when you use CDN
@import is a strange case, yes, I haven’t investigated what happens. In HTML5 spec seems like style’s onload needs to fire when @imported stuff is loaded too (at least that’s how I read “resource and its *critical* subresources”)
Use case: 1 page ajaxy apps.
1. initial load
2. click – XHR
3. JSON response that has: {html: ”, jsfiles: [], cssfiles: []}
4. You inject CSS files in DOM
5. once they are loaded, you inject html too
Otherwise user will see unstyled content. Images are less of a concern, whenever they arrive
August 30th, 2011 at 8:27 am
Stoyan, did you see a last comment on Zach Leatherman’s “Faking Onload for Link Elements” http://www.zachleat.com/web/load-css-dynamically/ page?
It’s made by Daniel Buchner and points to another solution (a trick) for firing Load event for CSS: “Link element load event support for CSS Style Sheet includes, finally!” http://www.backalleycoder.com/2011/03/20/link-tag-css-stylesheet-load-event/
September 27th, 2011 at 5:00 pm
@Vladimir, stylesheets are stored separately from images in webkit and FF. So in this case you get two downloads
November 8th, 2011 at 3:32 pm
Support for stylesheet link element events has landed in Firefox 8, enjoy! –> https://developer.mozilla.org/en/HTML/Element/link#Stylesheet_load_events
May 23rd, 2012 at 9:07 am
[...] doing some research and writing up my answer, I stumbled upon this link that explains everything you need to know about CSS, when it is loaded and how you can check for [...]
July 3rd, 2012 at 7:48 pm
One way to do this is check when an element has been styled by the CSS file itself. E.G, Have an element colored red by default, and then check when the CSS file itself colors it blue. setInterval is your friend here. ProTip. Have the style rule at the very bottom of the .css file so we know the whole sheet has been loaded. Haven’t wrote a script for this, but it should work in theory.
February 5th, 2013 at 12:46 am
Chrome Paint needs to be applied on a really positive mist.Customize Chrome purposes in your automobile or motorcycle with our Chrome spray paint Equipment. Usable for all conductive surfaces with none toxic or carcinogene supplies. Spray on Chrome for mirror – like end
February 11th, 2013 at 3:45 pm
[...] require two different CSS files. Both modules are requested at about the same time. We listen to onload of the CSS files. Expected behavior: whenever a module and its CSS dependency arrive – show that module. [...]
February 21st, 2013 at 1:37 pm
Seems like you actually understand a good deal pertaining to
this subject matter and it all shows throughout this article, called “When is a stylesheet really loaded?
/ Stoyan’s phpied.com”. Thanks -Therese
May 11th, 2013 at 2:57 am
Fastidious answers in return of this issue
with solid arguments and explaining all on the topic of that.
May 16th, 2013 at 8:53 am
Quality content is the key to attract the users to go to see
the website, that’s what this site is providing.
May 19th, 2013 at 2:36 pm
Hey there! Someone in my Facebook group shared this site with us so I came to check it out.
I’m definitely enjoying the information. I’m bookmarking and will be tweeting this to
my followers! Excellent blog and brilliant style and design.
May 21st, 2013 at 11:03 pm
Today, I went to the beach front with my children.
I found a sea shell and gave it to my 4 year old daughter and said “You can hear the ocean if you put this to your ear.” She put the shell
to her ear and screamed. There was a hermit crab inside and it pinched her ear.
She never wants to go back! LoL I know this
is entirely off topic but I had to tell someone!