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:
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) => {
const styles = document.getElementsByTagName("style");
const stylesToInject = styles[styles.length - 1];
const callback = () => {
const iframe = document.getElementById("your-iframe");
iframe.contentWindow.postMessage(stylesToInject.innerText, "*");
}
const observer = new MutationObserver(callback);
const config = { attributes: true, childList: true, subtree: true };
observer.observe(stylesToInject, config);
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: