Search code examples
javascripthtmliframeparallax

Parallax effect within a cross-origin iframe with pure JS?


So I have a document that contains an iframe element.

My document and the iframe have different sources.

I want to make the contents of this iframe look like parallax when loaded in the document.

For example, the document looks like this:

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<p>blah</p>
<p>blah</p>
<p>blah</p>
<p>blah</p>
<iframe src="parallax.html" width="200" height="200">
<p>blah</p>
<p>blah</p>
<p>blah</p>
</body>
</html>

And the parallax.html contains just an image that fits to the window size.

I thought that parallax.html should have script that determines the position of the window relatively to the screen and adjusts the image position. I've tried something, for example:

  • add scroll event listener to the top window, but since they're cross-domain, this is restricted
  • get document.body.scrollTop, but it always returns 0
  • get getBoundingClientRect() parameters, but top also returns 0

Is this even possible? Am I missing something? And yes, I have to make it with vanilla JS, no jQuery or other libraries.


Solution

  • As you note, the cross-origin restrictions makes this somewhat tricky. Your parent frame won't be able to access the internal variables or set listeners in its child frame, and vice versa. There's a very limited set of properties it can access, viewable here: https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy#Cross-origin_script_API_access

    Furthermore, I believe document.body.scrollTop and getBoundingClientRect() inside an iframe are relative to the frame itself, as if the frame were a mini-window. You probably won't be able to access the information you want in the iframe, but should be able to get it from the parent frame.

    Using postMessage

    The traditional way of passing information between cross-origin frames is to use iframe.contentWindow.postmessage.

    In this case, a simple implementation is to attach your scroll listener to your parent frame that sends a message to your child frame. It'd look something like this:

    In the parent document:

    var iframeElement = document.getElementById('my-parallax'); // give your iframe this id
    window.addEventListener('scroll', () => {
      var rect = iframeElement.getBoundingClientRect();
      var dataToSend = {
        scrollY: window.scrollY,
        left: rect.left, right: rect.right,
        bottom: rect.bottom, top: rect.top
      };
      var targetOrigin = '*'; // can change this if you know it
      iframeElement.contentWindow.postMessage(dataToSend, targetOrigin);
    });
    

    Then in parallax.html:

    window.addEventListener('message', function (e) {
      var data = e.data;
      if (e.source === window.parent) { // check if the sender was the parent
        // use data.scrollY, data.top... etc. here
      }
    });
    

    The caveat to this approach (and with postMessage in general) is that there is no guarantee that the UI of the iframe will update in sync of the parent window. The difference should be slight, unless there is significant computation on either frame. As far as I know, this is unavoidable when working with cross-domain iframes.