Skip to content
/docsfor

Pages

A remote component is an HTML document with the runtime in it. Everything on this page is optional.

The runtime

One classic script, first in the head:

index.html
<script src="https://widgets.example/rc-runtime.js"></script>

You rarely write it yourself. The serving layer adds it, the Vite plugin adds it, and the host inlines it for documents it shows itself. Before it, the serving layer also adds a doctype if the page has none.

The runtime handles layout, theme, focus and the channel to the host. A page that only shows content needs nothing else. Opened directly in a browser tab, it is a normal page: rc.props is empty and nothing changes.

window.rc

Props and overlays are on window.rc. It is an EventTarget. props-changed fires once when the first props arrive and again on every change. theme-changed fires when the host's styles change.

rc.props.value;                                 // current props
rc.addEventListener("props-changed", render);   // first props, then every change
await rc.props.onRate(4);                       // function props return Promises
rc.overlay({ kind: "popover", src: "menu.html", anchor: button });

Both events fire after the document is parsed, so a script at the end of the body is in time. The usual shape is a render function: call it once with rc.props, then on every props-changed.

This page draws its stars from a prop and reports clicks back. Click a star.

value = 3
widgets/stars.html
<!doctype html>
<html>
  <head>
    <style>
      .stars { display: flex; gap: 2px; }
      button { font: inherit; font-size: 20px; line-height: 1; width: 32px; height: 32px; padding: 0; border: 0; border-radius: var(--radius-sm); background: none; color: var(--color-text); cursor: pointer; }
      button:hover { background: var(--color-surface-muted); }
      button:focus-visible { outline: 2px solid var(--focus-ring, var(--color-accent)); outline-offset: 0; }
    </style>
  </head>
  <body>
    <div class="stars"></div>
    <script>
      const box = document.querySelector(".stars");

      function render() {
        const value = rc.props.value ?? 0;
        box.replaceChildren(
          ...[1, 2, 3, 4, 5].map((n) => {
            const button = document.createElement("button");
            button.textContent = n <= value ? "★" : "☆";
            button.ariaLabel = `${n} of 5`;
            button.ariaPressed = String(n === value);
            button.onclick = () => rc.props.onRate(n);
            return button;
          }),
        );
      }

      render();
      rc.addEventListener("props-changed", render);
    </script>
  </body>
</html>
host.tsx
const [value, setValue] = useState(3);

<RemoteComponent
  src="https://widgets.example/stars.html"
  value={value}
  onRate={async (n: number) => setValue(n)}
/>

A page with only text and buttons needs a few lines of the same. rsvp.html on the Introduction is one.

React

Any React app works as it is. To read props, use useProps() from @assistant-ui/remote-components-react. Keep component state in React as usual.

Counter.tsx
import { useState } from "react";
import { useProps } from "@assistant-ui/remote-components-react";

export function Counter() {
  const { label } = useProps<{ label: string }>();
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>{label}: {n}</button>;
}

The full list is in the Page API.