Component Isolation & Host Render Policy
By default, a Myop HTML component renders either as a same-origin iframe or, when it opts in, as a web component on the host document. Both run with access to the host page. That's ideal for trusted, first-party UI β but not for code you don't control.
The render policy gives the host app the final say over how (or whether) each component renders, and adds a new isolated rendering mode: a sandboxed, cross-origin iframe with no access to the host document β the safe way to render untrusted or marketplace components.
Render modesβ
Every HTML component resolves to one of these modes, in ascending order of isolation:
| Mode | What it is | Isolation |
|---|---|---|
'webComponent' | Custom element on the host document (Shadow DOM) | none β full host privileges |
'iframe' | Same-origin inline-HTML iframe (today's default) | none β same origin as host |
'isolatedIframe' | Sandboxed, cross-origin iframe | full β no host DOM / cookies / storage / window.parent |
false | Rendering is blocked | β |
The render policy hookβ
A host supplies a predicate that receives context about the component and returns the mode to use (or false to block).
import { hostSDK, RenderPolicy } from '@myop/sdk/host';
const policy: RenderPolicy = (ctx) => {
// ctx = { componentDefinition, skin, requestedMode, isWebComponent }
return isTrusted(ctx.componentDefinition)
? ctx.requestedMode // trusted: render as authored
: 'isolatedIframe'; // untrusted: sandbox it
};
hostSDK.setRenderPolicy(policy); // global, app-wide
RenderPolicyContext:
| Field | Meaning |
|---|---|
componentDefinition | The component being loaded (id, name, β¦) β key your trust decision on this. |
skin | The selected skin / variant. |
requestedMode | What the component asked for (marker β 'webComponent', isolation config β 'isolatedIframe', else 'iframe'). |
isWebComponent | Whether the HTML carries the <!-- myop-web-component --> marker. |
The policy may be async (return a Promise) β useful if trust requires a lookup.
Global vs. per-call precedenceβ
- Global:
hostSDK.setRenderPolicy(policy)applies to everyloadComponent. - Per-call:
loadComponent(config, container, { renderPolicy })β a per-call policy fully replaces the global one for that call.
This lets an app restrict globally and allow specific exceptions:
// Deny everything by defaultβ¦
hostSDK.setRenderPolicy(() => false);
// β¦except this one trusted component.
await hostSDK.loadComponent(trustedConfig, container, {
renderPolicy: () => 'iframe',
});
With no policy set, behavior is unchanged from before: the marker renders a web component, everything else an iframe.
Forcing isolation directlyβ
You don't need a full policy just to isolate. Set the isolation flag:
// Per load
await hostSDK.loadComponent(config, container, { data, isolation: true });
// Or on the loader/skin config
{ "type": "HTMLLoader", "HTML": "β¦", "shadowRootMode": "localFrame", "isolation": true }
A policy returning 'isolatedIframe' has the same effect and always wins over the config value.
What an isolated iframe actually doesβ
When a component renders isolated, the SDK:
- Creates the iframe with
sandbox="allow-scripts allow-forms allow-popups"β note the deliberate absence ofallow-same-origin. This gives the frame an opaque origin: the host cannot read itscontentDocument, and inside the framewindow.parentis cross-origin, with no access to the host's cookies,localStorage, or DOM. - Writes the component via
srcdoc(you cannotdocument.writean opaque-origin frame). - Injects a small postMessage bridge that translates the normal contract across the boundary:
- data-in β the host posts
myop:init; the bridge calls yourwindow.myop_init_interface(data)(plus a baked initial value for the first render), - data-out β your
window.myop_cta_handler(action, payload)is forwarded to the host as amyop:ctamessage, - resize β a
ResizeObserverreports content height to the host asmyop:size.
- data-in β the host posts
The author-facing contract is unchanged. Inside the frame you still define window.myop_init_interface and call window.myop_cta_handler exactly as in a normal HTML component β the SDK bridges it over postMessage. On the host side, component.props.myop_init_interface(...), getData(), and component.props.myop_cta_handler = β¦ all work the same; the SDK routes them through the bridge instead of same-origin access.
The marketplace patternβ
The motivating case: a host renders components authored by contributors outside its org. Isolate the untrusted ones; keep first-party UI fast.
hostSDK.setRenderPolicy((ctx) => {
const trusted = isFirstParty(ctx.componentDefinition);
if (trusted) return ctx.requestedMode; // web component / iframe, as authored
if (ctx.isWebComponent) return 'isolatedIframe'; // deny inline web component β sandbox
return 'isolatedIframe'; // untrusted iframe β sandbox too
});
Net effect: outside-org code never runs on the host document.
Important: what does NOT work isolatedβ
Isolation intentionally severs same-origin access. So a component that depends on the host being same-origin will break under isolation. This includes any component that:
- reads or writes
window.parent/window.topdirectly (rather than viapostMessage), - is itself a host of nested iframes it drives with
contentDocument/contentWindow(authoring tools, live-preview editors, WYSIWYG inspectors), - relies on the full iframe SDK ref/props/
ExecuteScriptprotocol.
Applying 'isolatedIframe' to everything also isolates first-party tools (for example, an in-app component editor). A sandboxed frame's own child iframes inherit the sandbox and become cross-origin to it β so nested-host tooling and same-origin handshakes stop working.
Isolation is a per-component decision: sandbox untrusted code, render first-party UI normally. That's exactly why this is a policy, not a global switch.
Blockingβ
Returning false from the policy blocks rendering entirely (loadComponent throws). Note that a web component denied by policy is blocked rather than downgraded β its HTML has no iframe-contract fallback, so it cannot be silently re-rendered as an iframe. Untrusted contributors who need to be sandboxed should ship iframe components (which the SDK can isolate), not web components.
Per-component in React / Vue / Angularβ
The framework components forward SDK loader options, so you can isolate a single
instance without a policy β pass isolation through loaderOptions:
// React
<MyopComponent componentId={id} data={data} loaderOptions={{ isolation: true }} on={onCta} />
<!-- Vue -->
<MyopComponent :componentId="id" :data="data" :loaderOptions="{ isolation: true }" :on="onCta" />
<!-- Angular -->
<myop-component [componentId]="id" [data]="data" [loaderOptions]="{ isolation: true }" [on]="onCta" />
Full framework guides: React Β· Vue Β· Angular.
A global setRenderPolicy still takes precedence over a per-instance loaderOptions.isolation.
See alsoβ
- Myop v2 Web Components β the on-host rendering mode this policy can allow or deny.