How to inject CSS into an iframe?

How to inject CSS into an iframe?

And why would you do that?

By default, stylesheets are bound to their document and don't leak into iframe elements (or vice versa). It does make sense for security and aesthetic reasons. A website would not want to be displayed with distorted styles when embedded on someone else's page.

So why would you do that?

I recently worked on a website that does not provide any form of hot-reload. Instead of building the assets manually, I decided to write the stylesheets in isolation.

So, I set up a storybook that would build the styles in real time and apply them to the stories. Each story would rely on an HTML file, whose content is copied from the website. Albeit simple, this approach was the first improvement in the developer experience.

That worked for a while until I had to apply styles on JavaScript-constructed elements (e.g. a Slider). I identified two solutions to this problem:

  • either make the JavaScript work in isolation

  • or don't work in isolation anymore

I chose the latter and started a new story with a single element: an iframe targeting the website running locally.

Connecting storybook to the iframe

To inject the styles built by storybook in the iframe, I needed to send data from the parent document to the document in the iframe. This can be achieved using the postMessage API.

Sending the message

On the "parent" side, sending data to the iframe is quite straightforward:

const iframe = document.getElementById("my-iframe");
iframe.contentWindow.postMessage(message, "*");

message

The message argument can basically take any serializable data. Our message will be a looong string containing our CSS.

"*"

This second argument aims to specify which domain can receive the message. The wildcard ("*") means anyone can receive the message.

Catching the message

If we want more than a message in a bottle, we have to set up some code on the iframe side. Receiving the message implies listening to the "message" event:

function receiveMessage(event) { ... }

window.addEventListener("message", receiveMessage, false);

The catch event contains:

data: the message.

origin: which domain the message was sent from.

source: object referencing the sender window and allowing two-way communication.

You need to find a way to add this script to the target website.
Don't forget to remove it before deploying it into production!

Adding the style to the document

We then use the received data as the innerText of a freshly created <style> element:

const styleTag = document.createElement("style");
document.head.appendChild(styleTag);

function receiveMessage(event) {
    styleTag.innerText = event.data;
}

window.addEventListener("message", receiveMessage, false);

Hot-reload

Now, the target website is able to receive and inject the message in a <style> tag.

But how do we get its content in the first place?

As I said in the introduction, the goal is to inject the styles into the iframe every time they change. I was able to achieve that using the MutationObserver API which can detect when storybook reloads the styles. I ended up writing the following decorator:

(story) => {
    // Get all style tags in storybook's scope
    const styles = document.getElementsByTagName("style");

    // In my case, I only need the last style tag
    const stylesToInject = styles[styles.length - 1];

    // This callback is called when changes are observed
    const callback = () => {
        const iframe = document.getElementById("your-iframe");
        iframe.contentWindow.postMessage(stylesToInject.innerText, "*");
    }

    // Create an observer instance linked to the callback function
    const observer = new MutationObserver(callback);

    // Observe style change
    const config = { attributes: true, childList: true, subtree: true };
    observer.observe(stylesToInject, config);

    // Render iframe
    return story();
};

Conclusion

Of course, those few lines are not optimized. The performance could be improved by sending only the difference. But, as perfectible as it may be, this trick did meaningfully improve the developer experience on my project.

Just keep in mind:

  • You need to be able to build your style in the storybook context

  • You may have to disable some CSP locks in order to display the website in the iframe.