Embed a widget in an existing page
Put a Beyond widget into a page that was not built with Beyond, from another origin, with the same dependency and shared styles it has in its own application: the import map, the registration, two instances, and what happens on removal and reinsertion.
- Availability: Experimental
- Evidence: Recorded run
- How-to guide
Keep the page you have
The host page keeps its markup, its styles and whatever framework it uses. The widget brings one custom element, its controller, its dependencies, its state and its styles, inside the shadow root of the element. Nothing of the page has to become a Beyond project, and no special embedding mode exists: the widget behaves as it does in its own application.
Get the import map from the origin that delivers the widget
A widget is a public module with a versioned identity. The origin that delivers it publishes, for that module, the import map of the module and everything it needs: its dependencies, the Widgets runtime, the adapter, the framework and the development runtime. In development it is the entry.json of the preview:
# The import map of the widget and its dependencies, as the development service publishes it for the
# widget module. Every address is relative to <endpoint>/preview/; a page on another origin makes them absolute.
curl -s "<endpoint>/preview/entry.json?entry=@testbed/widget-app/hello"The answer lists every module with its source (environment for what the service serves), its version and its address relative to <endpoint>/preview/. For a page on another origin, make each address absolute against that base; the acceptance does exactly that.
Write the host page
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="icon" href="data:,">
<title>An existing page that embeds a Beyond widget</title>
<style>
/* The styles of the host page: they must not reach the widget, and the widget's must not reach here */
body { font-family: Georgia, serif; }
h1 { color: rgb(200, 30, 30); }
.host-title { margin-bottom: 16px; }
</style>
<!--
The import map of the widget and its dependencies. It is what the delivery source publishes for the
widget module: in development, GET /preview/entry.json?entry=@testbed/widget-app/hello of the service.
Every address below is absolute, because this page is on another origin than the service.
-->
<script type="importmap" id="beyond-importmap">
{ "imports": {} }
</script>
</head>
<body>
<h1 class="host-title">This page was not built with Beyond</h1>
<p>The widget below is the same one the application shows. Two instances keep their own state.</p>
<hello-widget subject="Host page"></hello-widget>
<hello-widget subject="Second instance"></hello-widget>
<script type="module">
// Importing the module registers the element; the instances above mount when their module arrives.
// The development coordinator is optional: with it, edits reach the page without a reload.
const importmap = JSON.parse(document.getElementById('beyond-importmap').textContent);
if (importmap.imports['@beyond-js/local-2026/main'] && importmap.service) {
const { local } = await import('@beyond-js/local-2026/main');
await local.register({ origin: importmap.service, options: importmap.options, session: importmap.session });
}
await import('@testbed/widget-app/hello');
</script>
</body>
</html>Three things happen in the module script: the import map is read, the development runtime is registered so that edits reach the page (optional, and absent in production), and the widget module is imported, which registers <hello-widget>. The instances already in the markup mount when their module arrives.
What you get
- Two instances render with the same styles as in the application: the widget sheet (
text-brandblue heading, not the redh1rule of the host), the transitive badge sheet and the sharedglobalsheet of the package, every one of them loaded from the origin of the widget, not from the host page.BeyondWidget.hostis the identity prefix of the package as its module was loaded, so a shared stylesheet is addressed from the origin that serves the code. - The host page keeps its own styles: no rule of the widget reaches the document.
- Each instance keeps its own state: clicking one counter leaves the other at zero.
- Removing an element and inserting it again mounts the same controller in the same root, with no duplicated stylesheet links and no errors.
Production
The same page works against a production origin with the production options of the modules: minified, without the development runtime, and without the registration in the module script. Beyond CDN is the delivery source for that: a release base answers the import map of the release at <base>/importmap.json?target=browser&format=esm, with addresses relative to that URL, so a host page on another origin makes them absolute against the base before inlining them. The CDN is not hosted, so this page records the development origin only; see Strategies per environment.
Next
Render the widget on a server before the page loads it: Create a modular execution environment.