Background Info

On a recent client project, I was asked to create a login modal utilizing an <iframe> inside a modal pointing at the app's existing login page. For a number of tech reasons, this solution made sense vs. just creating a conventional login form component inside the modal. The main concern was keeping the experience as seamless as possible and to avoid looking like an iframe inside the modal.

The Setup

So I set out creating a basic modal and adding the iframe. This component was meant to be page agnostic, so I wanted to leave the call-to-action to open the modal flexible. To achieve this, I surrounded any children of the component in an invisible button. With this, any page this component would sit on could utilize any button, link, image, etc. to open the modal.

SCSS

.transparentButton {
  all: unset;
}

JavaScript

import { useEffect, useState } from "react";
import classNames from "classnames";
import styles from "./my-component.module.scss";

const MyComponent = ({ children }) => {
  const [modalOpen, setModalOpen] = useState(false);

  return (
    <div>
      <button
        className={styles.transparentButton}
        onClick={() => {
          setModalOpen(true);
        }}
      >
        {children}
      </button>
      
      <Modal isOpen={modalOpen}>
        <iframe 
          src="/login" 
          frameBorder="0" 
          height={} 
          width={} 
          scrolling="no" 
        />
      </Modal>
    </div>
  );
};

export default MyComponent;

And this completed the basic setup - a component that could take a call-to-action, open a modal, and load our existing login page within an iframe.

Dynamically Handling Iframe Height

But what about the width and height of the iframe? Because the underlying login page could change in the future, I wanted this to be handled dynamically. This would avoid needing to update the modal every time a content or style change happened on the login page. Luckily the width is easy, it's always 100% - but the height was a little more difficult. My solution was to utilize a ResizeObserver on the <main> element of the login page inside the onLoad event of the iframe. This method would require no changes to the login page and would notify my component of height changes with events without using any polling.

import { useEffect, useState } from "react";
import classNames from "classnames";
import styles from "./my-component.module.scss";

const MyComponent = ({ children }) => {
  const [iframeHeight, setIframeHeight] = useState(0);
  const [modalOpen, setModalOpen] = useState(false);

  return (
    <div>
      <button
        className={styles.transparentButton}
        onClick={() => {
          setModalOpen(true);
        }}
      >
        {children}
      </button>

      <Modal isOpen={modalOpen}>
        <iframe
          src="/login"
          frameBorder="0"
          height={iframeHeight}
          width="100%"
          scrolling="no"
          onLoad={event => {
            const { contentWindow } = event.target;
            const main = contentWindow.document.body.querySelector("main");

            // Because the login form has a dynamic height, observe any size changes and update the iframe height
            const resizeObserver = new ResizeObserver(entries => {
              entries.forEach(entry => {
                setIframeHeight(entry.contentRect.height);
              });
            });

            resizeObserver.observe(main);

            // When the iframe is hiden (i.e. modal is closed), remove any listeners
            const onVisibilityChange = () => {
              resizeObserver.disconnect();
              contentWindow.addEventListener("visibilitychange", onVisibilityChange);
            };

            // Add listener for when iframe is hiden (i.e. modal is closed)
            contentWindow.addEventListener("visibilitychange", onVisibilityChange);
          }}
        />
      </Modal>
    </div>
  );
};

export default MyComponent;

Iframe Communication

So now our modal opens, loads our page, and dynamically sets the height of the iframe to keep it a seamless experience to the user. But how does our component know once the user has logged in? This is where postMessage() comes in handy! It will require a small update to our login page, but we'll simply add a statement to post back to the parent page on login and catch that message inside our component.

Parent Page

// Within our parent page source code, we'll need to add the following line
window.parent.postMessage("onLogin", window.location.origin);

MyComponent

import { useEffect, useState } from "react";
import classNames from "classnames";
import styles from "./my-component.module.scss";

const MyComponent = ({ children }) => {
  const [iframeHeight, setIframeHeight] = useState(0);
  const [modalOpen, setModalOpen] = useState(false);

  const onLogin = () => {
    setModalOpen(false);
    // TODO Update parent page with authenticated user data
  };

  // Listen for `onLogin` message from login page
  useEffect(() => {
    const onMessage = event => {
      if (event.data === "onLogin") onLogin();
    };

    window.addEventListener("message", onMessage);
    return () => {
      window.removeEventListener("message", onMessage);
    };
  }, []);

  return (
    <div>
      <button
        className={styles.transparentButton}
        onClick={() => {
          setModalOpen(true);
        }}
      >
        {children}
      </button>

      <Modal isOpen={modalOpen}>
        <iframe
          src="/login"
          frameBorder="0"
          height={iframeHeight}
          width="100%"
          scrolling="no"
          onLoad={event => {
            const { contentWindow } = event.target;
            const main = contentWindow.document.body.querySelector("main");

            // Because the login form has a dynamic height, observe any size changes and update the iframe height
            const resizeObserver = new ResizeObserver(entries => {
              entries.forEach(entry => {
                setIframeHeight(entry.contentRect.height);
              });
            });

            resizeObserver.observe(main);

            // When the iframe is hiden (i.e. modal is closed), remove any listeners
            const onVisibilityChange = () => {
              resizeObserver.disconnect();
              contentWindow.addEventListener("visibilitychange", onVisibilityChange);
            };

            // Add listener for when iframe is hiden (i.e. modal is closed)
            contentWindow.addEventListener("visibilitychange", onVisibilityChange);
          }}
        />
      </Modal>
    </div>
  );
};

export default MyComponent;

And that's it! We now have a page agnostic login modal that utilizes the app's existing login page. Now this component could be used by any developer, on any page, without outside dependencies.

Future Updates

There are a couple features missing here that should be taken into consideration before releasing to production. Namely, while the iframe is loading what should be shown to the user and what should happen if the iframe has an error while loading? Luckily, with this setup both of these are not too complex and mostly come down to UI/UX. An isLoading state could be used to show/hide a loader, and the iframe's built-in onError event could be used to handle any loading issues.

As always, thanks for reading, and I hope this helps you out!