Myop v2 Web Components
A v2 Web Component is a Myop HTML component that renders as a native custom element (Web Components API + Shadow DOM) running directly on the host page — instead of inside an iframe.
Because there is no iframe boundary, a web component:
- shares the host's rendering context (no separate document, faster first paint),
- inherits the host's CSS custom properties (theme tokens) natively,
- talks to the host through a small, direct contract (a method in, a
CustomEventout).
It's the right choice for trusted, first-party UI. For untrusted or marketplace code, prefer an iframe or an isolated component — a web component runs with full host-page privileges.
How a component becomes a web component
The Myop SDK renders your HTML component as a custom element when both of these are true:
1. The HTML starts with the marker comment — it must be the very first thing in the file, before <!DOCTYPE>:
<!-- myop-web-component -->
2. It contains a <script id="myop-web-component"> that defines a custom element and sets window.__MYOP_TAG_NAME__ to the element's tag name.
When the marker is present, the SDK runs only the contents of <script id="myop-web-component">. It then does document.createElement(__MYOP_TAG_NAME__), appends the element to the host container, and drives it via the contract below.
Everything outside that script — a preview panel, playground controls, extra markup — is ignored in production. Put all of your styles, markup, and logic inside that one script (typically into the element's shadow root).
The contract
| Direction | Mechanism |
|---|---|
| Data in | The host calls element.myop_init_interface(data) — a method on your element. Called with no arguments, it should return the current data (the getData contract). |
| Data out | Your element dispatches a myop-cta CustomEvent: detail: { action, payload }, with bubbles: true and composed: true so it crosses the shadow boundary. |
These are the same myop_init_interface / CTA semantics used by HTML (iframe) components — only the transport differs (a direct method call and a DOM event, instead of the iframe's cross-frame channel).
Minimal example
<!-- myop-web-component -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script type="myop/types">
interface MyopInitData { title: string; count: number; }
interface MyopCtaPayloads { 'clicked': { count: number }; }
</script>
</head>
<body>
<script id="myop-web-component">
(function () {
const TAG = 'my-counter';
class MyCounter extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._data = { title: '', count: 0 };
}
connectedCallback() { this._render(); }
// Data-in. No-arg call returns current state (getData).
myop_init_interface(data) {
if (data === undefined) return this._data;
this._data = { title: data.title, count: data.count };
this._render();
}
_render() {
this.shadowRoot.innerHTML = `
`;
this.shadowRoot.getElementById('btn').onclick = () => {
// Data-out: a composed CustomEvent the host listens for.
this.dispatchEvent(new CustomEvent('myop-cta', {
bubbles: true, composed: true,
detail: { action: 'clicked', payload: { count: this._data.count } }
}));
};
}
}
// Guard: the script may run more than once across mounts.
if (!customElements.get(TAG)) customElements.define(TAG, MyCounter);
// REQUIRED — tells the SDK which tag to instantiate.
window.__MYOP_TAG_NAME__ = TAG;
})();
</script>
</body>
</html>
Consuming it from a host
A host receives and reacts to a web component exactly like any other Myop component. In @myop/react, for example, you pass data in and handle CTAs out via props — the SDK routes them to myop_init_interface and the myop-cta event for you.
At the SDK level:
const component = await hostSDK.loadComponent(config, container, { data: { title: 'Hi', count: 3 } });
// data-in updates + getData
component.props.myop_init_interface({ title: 'Updated', count: 4 });
const current = component.props.myop_init_interface(); // → { title: 'Updated', count: 4 }
// data-out
component.props.myop_cta_handler = (action, payload) => {
console.log('CTA', action, payload);
};
Theming
CSS custom properties pierce the shadow boundary, so the host's design tokens injected on :root (e.g. --H-3-1-1-primary-text, --FF-1-font-family) resolve inside your shadow DOM. Reference them with sensible fallbacks:
color: var(--H-3-1-1-primary-text, #222);
You can also apply per-instance token overrides on your root element from the incoming data.
Notes & caveats
- Guard
customElements.define. The extracted script can run on the host document more than once across mounts — wrap registration inif (!customElements.get(TAG)). - No iframe isolation. A web component runs with the host page's privileges (host DOM, globals, cookies). Only ship trusted code this way. For untrusted or marketplace components, use an iframe or an isolated iframe.
myop:sizeis not applied. Auto-sizing meta is an iframe concept; a custom element simply sizes to its content. Use:host { … }if you need to pin dimensions.- Denial by policy. A host can refuse to render web components via a render policy. Because web-component HTML has no iframe fallback contract, a denied web component is blocked (it does not silently downgrade).
When to use which
| Web component | Iframe (HTML) component | Isolated iframe | |
|---|---|---|---|
| Runs on host document | ✅ | ❌ (same-origin iframe) | ❌ (sandboxed, opaque origin) |
| Isolation from host | none | none (same-origin) | full |
| Best for | trusted, first-party UI | trusted UI needing an iframe | untrusted / marketplace code |
See Component Isolation & Host Render Policy for how a host controls which of these each component gets.