isolet: The Widget Runtime That Treats the Host Page Like a Hazard Zone
A deep dive into how Shadow DOM, CSP-aware style injection, and persistent framework adapters let one component ship safely across unknown websites.
- `isolet` treats widget distribution as an isolation problem first, and a framework problem second.
- Its default path uses Shadow DOM, CSS hardening, and CSP-aware style injection to survive hostile host pages without an iframe.
- The runtime preserves framework adapters across updates, so a widget can re-render instead of being torn down and rebuilt.
- Its real value is pragmatic middle ground: safer than a plain script embed, lighter than a second document.
When the host page becomes the enemy
Third-party widgets usually fail in boring ways. A global CSS rule leaks in, a host page blocks a style tag, or a framework gets rebuilt because the embed has no stable lifecycle. `isolet` is interesting because it treats those failures as the default case, not the edge case.
That changes the design brief. The goal is not just to render a component on someone else’s page. The goal is to ship UI into unknown territory and still trust the result.
What isolet actually ships
The repo is a pnpm monorepo with a small, purposeful shape: a core runtime, a CLI, and a demo app. That matters because it tells you `isolet` is not just a component library. It is a delivery system for isolated widgets.
The architecture is cleaner than the marketing language suggests. The host page provides a target node. `isolet` creates an isolated shell. The framework adapter keeps the actual widget identity alive across updates.
The isolation stack
The core move lives in `mount-container.ts`, where the runtime chooses among three modes: `shadow-dom`, `scoped`, and `none`. Shadow DOM is the default, which is the right choice if your first concern is style leakage.
The important detail is not just that a shadow root exists. It is that the host is hardened with a reset like `all: initial`, and mount operations are idempotent through a `data-isolet` marker. That means the runtime is trying to be both defensive and reusable.
| Concern | Plain script embed | isolet | iframe |
|---|---|---|---|
| Style leakage | High risk | Low in shadow-dom mode | Very low |
| CSP compatibility | Often fragile | Nonce-aware injection | Usually strong |
| Host integration | Direct but brittle | Native-feeling | Separated document |
| Performance overhead | Low | Low to moderate | Higher |
| Framework reuse | Depends on host | Preserved through adapters | Harder across the boundary |
| Isolation strength | Weak | Strong enough for many widgets | Strongest |
This is the middle ground that makes `isolet` worth studying. It does not try to out-iframe the iframe. It tries to remove the iframe tax when the workload does not need a second document.
Why updates do not tear the widget down
`create-isolet.ts` gives the runtime a simple lifecycle: mount, update, unmount. Under the hood, that simplicity is doing real work. State is tracked in a closure, props are shallow-merged, and the adapter can re-render without reconstructing the whole widget shell.
const widget = createIsolet({
mount(target, props) {
// adapter renders into a persistent root
},
});
widget.mount(root, { theme: 'light' });
widget.update({ locale: 'en' });
widget.unmount();
That persistence matters more than it sounds. If the root survives, framework internals survive with it. React can diff. Vanilla code can keep the same container. The widget feels stable because it actually is stable.
CSS is part of the bundle, not an afterthought
`isolet` treats CSS as something to package, not something to hope for. The build path rewrites CSS into text, then injects it into the isolated container rather than depending on linked stylesheets to behave on an unknown site.
That is where `detectCspNonce.ts` matters. A host page with a strict Content Security Policy can break a naive embed instantly. If the runtime can detect a nonce and attach styles correctly, it becomes viable in environments that usually reject widget experiments outright.
This is one of those details that looks small until you ship to a real customer. CSP is not a niche concern once the audience includes enterprise users.
The React adapter is more conservative than it looks
The React adapter is not trying to hide React. It is trying to preserve React. That means a persistent root, controlled re-renders, and a clean bridge between `update()` on the wrapper and the framework’s own diffing logic.
// conceptual shape
const root = createRoot(container);
root.render(<Widget {...props} />);
return {
update(nextProps) {
root.render(<Widget {...merge(props, nextProps)} />);
}
};
That restraint is a design strength. The adapter does less than a generic abstraction layer would do, which is why it can do the important thing well: keep the widget alive without making the framework feel alien.
Why this beats an iframe, and when it does not
`isolet` is not a universal replacement for iframes. If you need the strongest possible boundary, a separate document still wins. But if you want the ergonomics of a native component with much of the safety of isolation, this is a compelling trade.
| Option | Best at | Weakest at | When to choose it |
|---|---|---|---|
| Plain script embed | Lowest friction | Safety and containment | Internal surfaces, trusted hosts |
| isolet | Safe-enough isolation with native feel | Hard boundary guarantees | Embeds that need reuse, style safety, and framework adapters |
| iframe | Maximum isolation | Integration and overhead | Untrusted content, strict separation, legacy-safe sandboxes |
That is the real thesis. `isolet` is not about proving iframes obsolete. It is about making widget shipping feel less like a compromise and more like an engineering choice.





