Blog

Prevent CSS overwrites with iFrames

today February 27, 2023

In a previous post we saw how to aggregate email messages on a web page, so that we could read our newsletters on a single page, as opposed to fetching them from our overflowing inbox. One problem with injecting external HTML into your page is that the HTML can come with its own styling rules, and those rules can conflict with and overwrite other rules, whether in other HTML snippets or in your own page. This is happening because as the web browser renders the injected HTML, it processes any styling it encounters, and applies it – if relevant – to other snippets or to the containing page.

To prevent CSS from being overwritten, we could inject the HTML snippets into iFrames, which encapsulate and sandbox the inner code, so that it cannot impact any other code outside of the iFrame. Let's see how to accomplish that.

To get going, let's replicate the problem, so that we understand what we're dealing with here. All we need is a simple HTML page that references a simple Javascript file.

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Document</title>
    <style>
      body {
        background-color: #cccccc;
      }
      #messages-div {
        margin-top: 20px;
      }
      .container {
        width: 90%;
        margin: 10px auto;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h1>Load messages</h1>
      <button id="load">Load</button>
      <div id="messages-div"></div>
    </div>
    <script src="index.js"></script>
  </body>
</html>

In the HTML markup above, we define styling for our background color, which we will have overwritten by the HTML snippets that we will inject into "messages-div." We also have a button to trigger the injection.

Now, let's look at the accompanying Javascript:

document.addEventListener('DOMContentLoaded', () => {
  const messages = [];
  
  document.querySelector('#load').addEventListener('click', loadMessages);

  function loadMessages() {
    const messagesDiv = document.querySelector('#messages-div');
    messages.forEach((message) => {
      const htmlDiv = document.createElement('div');
      htmlDiv.innerHTML = message;
      messagesDiv.appendChild(htmlDiv);
    });
  }
});

In the Javascript code above, we define an empty messages array, which we will populate momentarily with HTML strings. We then define an event listener for our button, where a click will trigger the loadMessages function. That function simply gets the messages-div, iterates over the "messages" array, creates a Div element that contains the message, and finally appends the new div to the messages-div.

Here is the code for our HTML snippets, containing two HTML strings:

const messages = [
              `<!DOCTYPE html>
              <html lang="en">
                <head>
                  <meta charset="UTF-8" />
                  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
                  <title>CSS Overwrites</title>
                  <style>
                    body {
                      background-color: red;
                    }
                  </style>
                </head>
                <body>
                  <div class="container">
                    <h1>Message 1</h1>
                    <p>This is the first message</p>
                    <p>This is the first message</p>
                    <p>This is the first message</p>
                    <p>This is the first message</p>
                    <p>This is the first message</p>
                  </div>
                </body>
              </html>
              `,
              `<!DOCTYPE html>
              <html lang="en">
                <head>
                  <meta charset="UTF-8" />
                  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
                  <title>CSS Overwrites</title>
                  <style>
                    body {
                      background-color: blue;
                    }
                  </style>
                </head>
                <body>
                  <div class="container">
                    <h1>Message 2</h1>
                    <p>This is the second message</p>
                    <p>This is the second message</p>
                    <p>This is the second message</p>
                    <p>This is the second message</p>
                    <p>This is the second message</p>
                  </div>
                </body>
              </html>
              `,
            ];

You'll notice that the HTML strings are similar; the only noticeable difference is the background color: one is red and the other is blue.

If you now view the HTML page in your browser, you'll see that the light-grey background color that we defined for our page is overwritten to blue. That is because the last HTML snippets contains blue color styling for its body. That generic definition impacts our own page's background color.

To overcome this problem, let's inject the HTML snippets into iFrames that will isolate their styling:

  function loadMessages() {
    const messagesDiv = document.querySelector('#messages-div');
    messages.forEach((message) => {
      const iframe = document.createElement('iFrame');
      iframe.scrolling = 'no';
      iframe.style = 'width: 100%; border: none;';
      iframe.onload = function () {
        var doc = this.contentWindow.document;
        doc.body.innerHTML = message;
        iframe.height = '';
        iframe.height = iframe.contentWindow.document.body.scrollHeight + 'px';
      };
      messagesDiv.appendChild(iframe);
    });
  }

In the revised function above, we create an iFrame element for each message. We prevent its own scrolling, and set its width to 100% and no borders. We then define the "onload" function that sets the message HTML to the iFrame's doc's body. We set the iFrame height to its content's height and append the iFrame to the "messages-div."

If you reload the HTML page, you'll notice that the parent page retains its light-grey background color, and the two HTML snippets have their own background colors. Now everything is well isolated.

Happy HTML injection!