I am working on a widget that will be embedded into other applications via an iframe (I believe I have to use an iframe because my widget will issue endpoint calls to its own API service, so the iframe needs to have the same origin as this API service).
I have something that works to some extent so far, with the following architecture:
1. A loader.js
script
(() => {
const script = document.currentScript;
const loadWidget = () => {
const widget = document.createElement("div");
const widgetStyle = widget.style;
widgetStyle.display = "none";
widgetStyle.boxSizing = "border-box";
widgetStyle.width = "100%";
widgetStyle.height = "100%";
widgetStyle.position = "fixed";
widgetStyle.top = "0";
widgetStyle.left = "0";
const iframe = document.createElement("iframe");
const iframeStyle = iframe.style;
iframeStyle.boxSizing = "borderBox";
iframeStyle.position = "absolute";
iframeStyle.right = 0;
iframeStyle.top = 0;
iframeStyle.width = "100%";
iframeStyle.height = "100%";
iframeStyle.border = 0;
iframeStyle.margin = 0;
iframeStyle.padding = 0;
widget.appendChild(iframe);
iframe.addEventListener("load", () => {
widgetStyle.display = "block";
});
const widgetUrl = `http://localhost:5173`;
iframe.src = widgetUrl;
document.body.appendChild(widget);
};
if (document.readyState === "complete") {
loadWidget();
} else {
document.addEventListener("readystatechange", () => {
if (document.readyState === "complete") {
loadWidget();
}
});
}
})();
This loader.js
script can be embedded like so:
<script async src="https://localhost:3000/widget" data-license="123"></script>
2. A React.js app that renders the widget
(I am using Vite for this app)
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Widget</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/widget.tsx"></script>
</body>
</html>
widget.tsx
function Widget() {
/* This renders the widget which has the following style:
position: fixed;
top: 50%;
left: 0;
transform: translate(calc(-100% + 38px), -50%);
display: flex;
align-items: center;
*/
}
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<Widget />
</React.StrictMode>,
);
The issue that I’m running into is that my React entrypoint is rendering a full HTML body with 100% width/100% height and so the clicks are all being hijacked by my widget even though it only renders a few different divs with a high z-index which are spread out over the entire page with position fixed.
I have been able to almost make this work by having pointer-events: none
on my iframe
, which makes the clicks go through to the client app but then no clicks can be processed by my iframe (I also tried pointer-events: auto
but that doesn’t work).
I would like to figure out if my overall architecture even makes sense. Should I rethink this?