GemmaPoddocs
Guides

Embed in React

Default Preact widget inside a React app, or headless with your own UI.

Two flavours, depending on whether you want GemmaPod's built-in chat widget or your own:

Flavour 1 — Default widget inside a React app

Use this if you're happy with the shim's bundled Preact chat UI and just want it mounted into a <div> your React tree renders.

import { useEffect, useRef } from "react";

export function Pod() {
  const host = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    if (!host.current) return;
    let mounted: { destroy(): Promise<void> } | null = null;
    (window as any).GemmaPod
      .mountPod(host.current, config)
      .then((m: any) => (mounted = m));
    return () => void mounted?.destroy();
  }, []);

  return <div ref={host} />;
}

Wire the IIFE in index.html:

<script src="https://cdn.jsdelivr.net/npm/@gemmapod/embed@0.1.0/dist/gemmapod-shim.iife.js"></script>

Flavour 2 — Headless (your own UI)

Use this for CopilotKit-shaped hosts, AI SDK chat surfaces, or any custom React UI. Load the smaller runtime-only IIFE and pass ui: "none" to mountPod.

<script src="https://cdn.jsdelivr.net/npm/@gemmapod/embed@0.1.0/dist/gemmapod-runtime.iife.js"></script>
import { useEffect, useRef, useState } from "react";

export function Pod() {
  const [lines, setLines] = useState<string[]>([]);
  const [input, setInput] = useState("");
  const runtimeRef = useRef<any>(null);
  const fallbackHost = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    let m: any;
    (async () => {
      m = await (window as any).GemmaPod.mountPod(null, config, {
        ui: "none",
        fallbackUi: "default",
        fallbackMountParent: fallbackHost.current ?? undefined,
      });
      runtimeRef.current = m.runtime;
      m.runtime.events.on("ui.event", ({ event }: any) => {
        if (event.type === "TEXT_MESSAGE_CONTENT") {
          setLines((cur) => [...cur, event.delta]);
        }
      });
    })();
    return () => void m?.destroy();
  }, []);

  return (
    <div>
      <div>{lines.join("")}</div>
      <input value={input} onChange={(e) => setInput(e.target.value)} />
      <button
        onClick={async () => {
          for await (const _ of runtimeRef.current.chat.stream(input)) {}
          setInput("");
        }}
      >
        Send
      </button>
      <div ref={fallbackHost} />
    </div>
  );
}

The full version is at examples/react-headless.

Picking a flavour

You want…Use
The shim's built-in chat UIFlavour 1
Your own transcript + composerFlavour 2
A CopilotKit-shaped event handlerFlavour 2 + AG-UI bridge
To render a custom 3D companion / cart / dashboardFlavour 2 + STATE_SNAPSHOT

What's next