Skip to main content

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 CustomEvent out).

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.

Only that script ships to production

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

DirectionMechanism
Data inThe 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 outYour 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 = `
<style>
:host { display: block; font-family: sans-serif; }
.title { color: var(--H-3-1-1-primary-text, #222); font-weight: 600; }
.count { font-size: 28px; color: var(--H-1-1-1-primary-accent-button, #6366f1); }
button { cursor: pointer; }
</style>
<div class="title">${this._data.title}</div>
<div class="count">${this._data.count}</div>
<button id="btn">Click me</button>`;

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 in if (!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:size is 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 componentIframe (HTML) componentIsolated iframe
Runs on host document❌ (same-origin iframe)❌ (sandboxed, opaque origin)
Isolation from hostnonenone (same-origin)full
Best fortrusted, first-party UItrusted UI needing an iframeuntrusted / marketplace code

See Component Isolation & Host Render Policy for how a host controls which of these each component gets.