What would happen if a <iframe> tag on a page pointed to the same page ? In HTML, iframes are elements designed to display documents under an URL, as if we would load it as we normally do in our browsers. See it as a small window within a web document which can display another web document. For instance, what you see here is an <iframe> pointing to the homepage of this website, https://ciphrd.com:
On a side note, the technique I am going to present is not really “feedback” per say, it’s more like a regular loop, but it reminds so much of a feedback effect that I found the name appropriate. Source code at the end.
1. The most basic <iframe> loop
Just as a quick reminder, this is how an iframe can be used:
<iframe src="https://ciphrd.com"></iframe>
The browser, when coming accros this tag, reads the src attribute and loads the web document under the URL into the area available for the iframe. Such area can be defined with CSS rules, or by setting a width and height attribute to the tag. For more informations, see the article about html iframe on W3Schools [1].
Now, the idea behind <iframe> feedback: let the src attribute point to the same location as the page in which the <iframe> is displayed. To experiment with such idea, I built a very simple html page and I hosted it on a local server at http://localhost:8080/
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>iframe loop</title>
</head>
<body>
<h1>iframe loop</h1>
<iframe src="http://localhost:8080"></iframe>
</body>
</html>
By loading http://localhost:8080/ in the browser, the document should be displayed and the <iframe> should display the same document, over and over. With some very simple rules we can define the size of our iframe:
iframe {
width: 500px;
height: 320px;
}
h1 {
margin: 20px;
}
Let’s see how it is handled by chrome (88.0.4324.150):
Oh, that’s weird. It looks like the browser only loads the first <iframe> and then prevents an infinite loop from happening by removing the contents of the 2nd nested <iframe>. A quick inspection of the HTML tree will confirm our hypothesis:
So, the hypothesis is verified. We cannot nest infinite <iframe> tags because the browser prevents it, which is normal if we think about it. We wouldn’t want our computer to freeze if we were to load a webpage using such a trick.
2. How to nest any number of <iframe>
The issue here, is that the <iframe> tag is already present on the document loaded by the browser. Let’s see what happens if we insert the tag after the page has loaded, using javascript. Let’s reduce our HTML page to the following:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>Iframe loop</title>
</head>
<body>
<h1>iframe loop</h1>
</body>
</html>
And let’s write some Javascript which will insert the tag as soon as the page is loaded:
const iframe = document.createElement('iframe')
iframe.src = location.href
document.body.append(iframe)
The following javascript code needs to be loaded in the page somehow, the <script> tag can be used in that matter. However, because I am using webpack, my javascript gets automatically added to the document. The project I created for you to experiment with iframe feedback has the same feature, I’ll explain in on the last part of this article. For now, just assume that the javascript is added to the page. Let’s see what happens now:
The same issue remains. Maybe we can try with a delay ?
setTimeout(() => {
// this code will be executed 1000ms after the script has loaded
const iframe = document.createElement('iframe')
iframe.src = location.href
document.body.append(iframe)
}, 1000)
Okay, so it looks like we cannot just nest infinite <iframe> this way. Maybe we need to trick the browser into thinking that the page displayed is not the same. To do that, I configured my local server so that any path provided after the URL will point to the index.html. So, if I try to load http://localhost:8080/home, http://localhost:8080/cucumber, http://localhost:8080/zeagjaga/ergjergeg/erg,… , the same index.html file will be loaded. We can now configure our script to set the src tag to the same domain plus a random string of characters:
setTimeout(() => {
const iframe = document.createElement('iframe')
// adds a random string of 7 characters after the domain
iframe.src = location.origin + '/' + Math.random().toFixed(7).replace('.', '')
document.body.append(iframe)
}, 1000)
Let’s see what happens…
Hehehe… It’s working. A quick inspection of the DOM can confirm this hypothesis:
Wonderful ! So, to nest “self-replicating” <iframe> tags, the constraints are the following:
- the URL path must be different
- a server must be configured so that different URL points to the same document
That’s it. We don’t even need the delay before inserting the <iframe>. I will keep it though, I don’t really want the browser to crash because of that (not that it will, but if we want to do more fun stuff than just adding a new <iframe>, it might).
3. Having fun
Now that we found a way to load as many nested <iframe> as we want, let’s have some fun ! First, let’s change the size of an <iframe> so that its dimensions are relative to the viewport area it renders to:
iframe {
width: 90vw; // 90% of the width
height: 90vh; // 90% of the height
}
h1 {
position: absolute; // makes the title "float" on the top left
}
Which will give us a way better result in terms of space occupation of the iframe elements:
I guess you understand now why I called this technique iframe feedback. It just looks like it. However, this is by no mean true feedback.
We can remove the title and assign a random color to the background of the body to create a simple color effect:
// set a random color to the body background, x|0 removes the decimal part of x
document.body.style.backgroundColor = `rgba(${(Math.random()*255)|0}, ${(Math.random()*255)|0}, ${(Math.random()*255)|0}, 1)`
setTimeout(() => {
const iframe = document.createElement('iframe')
iframe.src = location.origin + '/' + Math.random().toFixed(7).replace('.', '')
document.body.append(iframe)
}, 200)
If you are familiar with writing web applications, you may see now how much fun we can have with this technique. Let’s try adding some CSS animations [2] to the position of the <iframe>:
body {
margin: 0;
overflow: hidden; // hides the scrollbar if window gets too small
width: 100vw; // set the body to the same size as the viewport
height: 100vh;
}
@keyframes move {
from {
top: 0;
left: 0;
}
to {
top: 10vh;
left: 10vw;
}
}
iframe {
width: 90vw; // 90% of the width
height: 90vh; // 90% of the height
position: absolute;
// the iframe with receive animation move and loops by alternating directions
animation: move 1s ease-in-out 0s infinite alternate-reverse;
}
I’m not going into in-depth details about what this CSS animation does. But here is the result:
We can add some rotation to the animation:
@keyframes move {
from {
top: 0;
left: 0;
transform: rotate(-20deg);
}
to {
top: 10vh;
left: 10vw;
transform: rotate(20deg);
}
}
Pretty fun ! What’s great is that we can take fulfill of all the sweet features made available by our browsers to have fun with this technique. Some issues remain though:
- <iframe> tags are added indefinitely
- each <iframe> is independent, we have no way to know its position in the tree
Let’s solve those issues and create a basic starting point from which experimentation with this technique can safely be done !
4. Improving the solution
First of all, we can define an easy solution to prevent iframes from being inserted indefinitely. If the viewport size reaches 0, there is no need to insert an iframe because it won’t have any effect on the screen:
try {
if (window.innerWidth <= 1 && window.innerHeight <= 1) {
throw new Error() // exit
}
// otherwise add the iframe as usual
// ...
}
catch (error) {} // exit
The trick I am using here provides a way to exit the main script by using the try… catch mechanism. If we throw an error in the try{} block, what’s after will be skipped and the catch{} block will be triggered. If there were no catch block, the script would still stop but the error would be sent to the console as “Uncaught Error”. Yes, that’s what it is.
We now have a safe guard to prevent infinite looping in most cases. What we could need is to find a way to get the index of the document in the <iframe> nested nightmare. Remember, we currently use the following trick to call for a new iframe:
iframe.src = location.origin + '/' + Math.random().toFixed(7).replace('.', '')
The location object [3] has different properties we can use to solve our problem. Let’s see what the location object is on the page https://ciphrd.com/category/laboratory/ :
It seems like the origin property stores whats before the first / and the pathname stores what’s after the origin (https://ciphrd.com), including the first /. Let’s use those properties to keep track of the index of each frame:
// gets what's after the first / , so the first url path
// or 0 if the path is empty (so the first document)
let idx = location.pathname.split('/')[1] || 0
idx = parseInt(idx)
// insert <iframe> after 200 ms
setTimeout(() => {
const iframe = document.createElement('iframe')
iframe.src = location.origin + '/' + (idx+1)
document.body.append(iframe)
}, 200)
This code will set the src of the next iframe based on the index in its URL, so it increases iteratively. This is what the DOM looks like with this new trick:
A simple but effective technique to keep track of the index. We can then use this index as we wish for any purpose.
As a matter of fact, we can use this trick to send more data than just the ID to the next frame. We can use the URL to send any data that fits in an URL. However, I found a more practical solution to send data between frames. The localStorage object [4] can be used to save data between sessions, and a localStorage objects allows us to store persistent data under a domain. That’s good, because all of our pages are under the same domain, which means that they have access to the same Storage object.
let idx = location.pathname.split('/')[1] || 0
idx = parseInt(idx)
const lastFrameData = localStorage.getItem('data-key') // get data from last frame
localStorage.setItem('data-key', "any data for next frame") // set data for next frame
We first get the item under key we defined, then we can set the data for the next frame (it can be anything). You should note that if you plan on using this technique, you need to take into account that if the index is 0, we cannot get the data from the localStorage because it doesn’t exists (or if it does, it comes from a previous session, either way it needs a reset).
The localStorage can be used for any purpose, not only sending and updating data between frames. For instance, if you may want to initialize values on the first frame and only access those values on the next iframes.
5. Some basic experiments
I haven’t explored this system a lot, here are some results with CSS transform animations:
6. The Open-Source boilerplate
I set up and made available a boilerplate for you to experiment with iframe feedback. The boilerplate is very simple, but it does all the job of setting a local server, having all the path pointing to the index, and a basic <iframe> insertion mechanism. Feel free to experiment with it, all the installation and usage details can be found with the source code:
https://github.com/bcrespy/iframe-feedback
Thanks for reading through, feel free to post your experiments in the comments !
References
- HTML iframe, W3Schools
- Using CSS Animations, MDN Web Docs
- Location, MDN Web Docs
- window.localStorage, MDN Web Docs
Why stop at embedding the frame once? Embedding it twice could make for some interesting- and CPU fan taxing- results.
As mentioned, this is opened for experimentation 🙂 There are many ideas and ways to leverage this technique
What about iframes in two pages? A ->B, B ->A
It would work for sure. Same thing can also be emulated by using different effects based on the index of the
Well done ! my favorite one is the scaling loop above. I was wandering if you have been thinking of frames (instead of iframes) at some point ? Maybe toying with
Cross-origin resource sharing ?
Thanks ! Yes this is how I ended up making the analogy with regular feedback, where each frame is derived from the previous one.