Show an HTML page from another origin inside your app. It runs in a sandboxed iframe and lays out like an inline element.
Info
The packages are not on npm yet. In the assistant-ui monorepo, depend on them with workspace:*.
A remote component is an HTML document with one script in it, rc-runtime.js. Your app renders it with <RemoteComponent>. The page gets its width from your layout and its height from its content. It inherits your fonts and colors, reads your props and calls your functions.
Any HTML works. A page with no doctype, a fragment, a page a model just wrote, a built React app. The page is the component, and its <html> element is the box in your layout.
Install
npm install @assistant-ui/remote-components-host-react @assistant-ui/remote-components-serveThe host package goes in the app that renders remotes. The serving layer goes on the server that serves them.
Quickstart
1. Write a page
Plain HTML. Props are on rc.props, function props call back into your app, and props-changed fires when they change. No build step.
<!-- No doctype, no build step. The serving layer adds the doctype and the runtime. -->
<style>
.event { font-weight: 600; }
.when { color: var(--color-text-muted); }
.row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
button { font: inherit; color: inherit; height: 28px; padding: 0 10px; border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-surface); cursor: pointer; }
button:hover { background: var(--color-surface-muted); }
button:focus-visible { outline: 2px solid var(--focus-ring, var(--color-accent)); outline-offset: 2px; }
</style>
<div><span class="event">Team lunch</span> <span class="when">Friday, 12:30</span></div>
<div class="row">
<button value="yes">Going</button>
<button value="no">Can't make it</button>
</div>
<script>
const render = ({ event = "Team lunch", when = "Friday, 12:30" }) => {
document.querySelector(".event").textContent = event;
document.querySelector(".when").textContent = when;
};
render(rc.props);
rc.addEventListener("props-changed", () => render(rc.props));
for (const button of document.querySelectorAll("button")) {
button.onclick = () => rc.props.onReply(button.value);
}
</script>2. Serve it
The serving layer adds the doctype, the runtime tag and a Content Security Policy to every page under root. It also serves rc-runtime.js.
import http from "node:http";
import { remoteComponentsMiddleware } from "@assistant-ui/remote-components-serve/node";
const serve = remoteComponentsMiddleware({ root: new URL("./widgets", import.meta.url).pathname });
http
.createServer((req, res) => serve(req, res, () => res.writeHead(404).end()))
.listen(5173, "127.0.0.1");Serve remotes from a different site than your app. In development, use 127.0.0.1 for remotes and localhost for the app.
3. Render it
import { useState } from "react";
import { RemoteComponent } from "@assistant-ui/remote-components-host-react";
export function Lunch() {
const [reply, setReply] = useState<string | null>(null);
return (
<>
<RemoteComponent
src="http://127.0.0.1:5173/rsvp.html"
event="Design sync"
when="Thursday, 10:00"
onReply={async (answer: string) => setReply(answer)}
/>
<span>onReply({reply})</span>
</>
);
}This site has no second origin to serve pages from, so the examples in these docs pass the same documents with the html prop. They run in an opaque sandboxed frame with the runtime inlined.
Click Going. The page calls onReply, which runs in your app.
With React
The fastest path is the CLI. It builds one component file into one HTML file, with no config:
npx remote-components build rating.tsxA React app is a page too. Add the runtime tag to its index.html and read props with useProps() from @assistant-ui/remote-components-react. In an existing Vite app, the Vite plugin turns each component file into its own page.
import { useState } from "react";
import { useProps } from "@assistant-ui/remote-components-react";
export default function Counter() {
const { label, onChange } = useProps<{ label: string; onChange?: (n: number) => Promise<void> }>();
const [n, setN] = useState(0);
return (
<button
onClick={() => {
setN(n + 1);
void onChange?.(n + 1);
}}
>
{label}: {n}
</button>
);
}<RemoteComponent src="http://127.0.0.1:5173/counter/" label="Clicks" />The counter below is the same component written as a plain page.
<button id="count"></button>
<script>
const button = document.querySelector("#count");
let n = 0;
const render = () => (button.textContent = `${rc.props.label ?? "Clicks"}: ${n}`);
render();
rc.addEventListener("props-changed", render);
button.onclick = () => {
n += 1;
render();
void rc.props.onChange?.(n);
};
</script>No server
You can skip the server. <RemoteComponent html={…} /> shows a string of HTML in an opaque frame, with the runtime inlined. See Generated HTML.
Packages
| Package | |
|---|---|
@assistant-ui/remote-components-runtime | rc-runtime.js, the script inside the page. It exposes window.rc. No framework. |
@assistant-ui/remote-components-host-react | <RemoteComponent>, the provider and overlays. |
@assistant-ui/remote-components-serve | Normalizes any HTML into a remote. Node middleware at /node. |
@assistant-ui/remote-components-react | Optional hooks for React pages. |
@assistant-ui/remote-components-cli | remote-components build: one component to one HTML file. |
@assistant-ui/remote-components-vite | Optional Vite plugin. |
@assistant-ui/remote-components-protocol | Messages, value codec, channel and validation. Internal. |
Next: CLI builds a React component. Pages covers what a page can do. Layout covers how it fits.