One thing that sucks about using an iframe is that you can't tell from the parent context whether it loaded correctly or not.

This makes it hard to provide good UX when users are offline (and your site is a PWA) or when the iframe fails to load for some other reason.

Use case

The Vue.js Developers website has a Vimeo video embedded on the homepage, which utilizes an iframe to provide the video player. This is how it looks when the iframe loads and everything works correctly:

I'm in Indonesia right now, and the ISP I'm using here, Telkomsel, has decided to block Vimeo for morality reasons. This means I, and Telkomsel's 121 million other users, get this ugly dead iframe front and center when looking at the homepage:

It also looks like this when offline users are taking advantage of the PWA features of this site.

I'd love to show some kind of fallback content if the iframe fails...but how?

Why you can't detect iframe errors

There's no accurate way of programmatically detecting whether or not an iframe loaded from the parent context. iframe elements don't emit an error event, so you can't use JavaScript to listen for an error.

But what if we got clever and made an AJAX call to the iframe URL? If it returns 404 we'd know the iframe was unavailable.

Nope. Due to the "same origin" security policy implemented by your browser, you can't use AJAX across domains like that. So your AJAX call will fail even if the iframe loads fine.

A crude solution

Here's a solution I've implemented that roughly does the job. This uses Vue.js but you could easily do this with pure JS or another framework.

The idea is that I use an image (text or some other content would work too) as fallback content. I show the fallback while the iframe is loading, then if I think it loaded okay, I replace the fallback content with the loaded iframe.

But how do I know if the iframe successfully loaded? While iframes don't have an error event, they do have a load event. By timing the load event, you can have a crude indication as to whether it loaded correctly or not.

The reason this works is that if the iframe fails to load, it will probably timeout and the load event will take an unusually long time to fire.

How I implemented it

Firstly, I created a wrapper element and put both the fallback content and the iframe in it.

<div class="wrapper"> <img class="fallback" v-if="!iframeLoaded"> <iframe src="..." :class="{ 'loading' : !iframeLoaded }" @load="iframeLoad"/> </div>

I created a boolean variable iframeLoaded which has two uses: firstly to dynamically show the fallback content, and secondly to hide the iframe with a dynamically bound CSS class.

I then listen to the native load event on the iframe and handle the event with a method iframeLoad . From here, I can inspect the timeStamp value, and if it's less than the selected threshold (I've gone with 10 seconds), I assume the iframe loaded successfully and toggle iframeLoaded .

export default { data: () => ({ iframeLoaded: false }), methods: { iframeLoad(e) { if (e.timeStamp < 10000) { this.iframeLoaded = true; } } } });

You'll see the loading class for the iframe gives it a height and width of zero, ensuring it can't be seen unless and until it properly loads.

.wrapper { position: relative } .wrapper > * { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } .wrapper > iframe.loading { width: 0%; height: 0%; }

Flaw in the approach

What if the user is part of the ~30% of people around the world still using a 2G connection? The big problem with this approach is that I'm bound to get false positives no matter what the timestamp threshold is set to.

If you're going to use this approach, make sure your fallback content is a net improvement on user experience even with the occasional false positive.

For Vue.js Developers, I believe that's the case. Someone on a connection slow enough that it takes more than 10 seconds to load the iframe probably won't want to stream a video anyway. The fallback content I've used is a local version of the video thumbnail, so worst-case scenario the user just sees that. 🤷