SDK

Components.

@onpilot/react ships four embed surfaces. They all render the same chat; they differ in the chrome around it.

ComponentChromeUse when
CopilotBubbleFloating button + popupPublic website, anonymous or logged-in users
CopilotSidebarSDK toggle + slide-out panelQuick admin embed, you don't want to design the chrome
CopilotInlineNone — fills its containerEmbedded in a page region, dashboard tab, modal
CopilotPanelNone — headlessFull layout control (custom toggle, top-bar pattern)

All four accept the same base props: identityToken (or identityTokenProvider), theme, primaryColor, context, and onOpen / onClose / onReady / onMessage / onError callbacks. Most styling — agent name, instructions, suggestions — is set in the dashboard, not via props.

Bubble

<CopilotBubble />

Floating button in the corner of the page. Opens a popup on click.

tsx
import { CopilotBubble } from "@onpilot/react";

<CopilotBubble
  identityToken={token}
  position="bottom-right"
  width={400}
  height={600}
/>;

Extra props: position (bottom-right | bottom-left), width, height, defaultOpen.

Sidebar

<CopilotSidebar />

Fixed side panel with the SDK's own toggle button. Pushes page content over when opened (optional).

tsx
import { CopilotSidebar } from "@onpilot/react";

<CopilotSidebar
  identityToken={token}
  position="right"
  width={420}
  pushContent
/>;

Extra props: position (left | right), width, pushContent, enabled, defaultOpen, toggleButton.

Inline

<CopilotInline />

No chrome. Fills its parent container. The chat is just a block in your page.

tsx
import { CopilotInline } from "@onpilot/react";

<div className="h-[600px]">
  <CopilotInline identityToken={token} />
</div>;

Panel

<CopilotPanel />

Headless. No toggle, no built-in close behavior. You control when it mounts and unmounts.

tsx
import { CopilotPanel } from "@onpilot/react";

const [open, setOpen] = useState(false);

<button onClick={() => setOpen((o) => !o)}>Assistant</button>;
{open && <CopilotPanel identityToken={token} width={420} height="100vh" />}
useOnPilot() doesn't work inside CopilotPanel — there's no provider context. Use CopilotInline or CopilotSidebar if you need the hook.

Custom layouts

Top-bar assistant pattern

A button in your top bar opens a chat panel anchored to the right edge of the viewport. The iframe's own X button emits a postMessage — you listen for it and update your state.

tsx
"use client";

import { useEffect, useState } from "react";
import { CopilotPanel } from "@onpilot/react";

const DASHBOARD_URL = "https://chat.onpilot.ai";

export function AssistantButton({ identityToken }: { identityToken: string }) {
  const [open, setOpen] = useState(false);

  useEffect(() => {
    const dashboardOrigin = new URL(DASHBOARD_URL).origin;
    function handler(event: MessageEvent) {
      if (event.origin !== dashboardOrigin) return;
      if (event.data?.type === "onpilot:close") setOpen(false);
    }
    window.addEventListener("message", handler);
    return () => window.removeEventListener("message", handler);
  }, []);

  return (
    <>
      <button onClick={() => setOpen((o) => !o)}>Assistant</button>
      {open && (
        <div className="fixed right-0 top-0 h-screen w-[420px] shadow-xl">
          <CopilotPanel identityToken={identityToken} width="100%" height="100%" />
        </div>
      )}
    </>
  );
}
Always validate event.origin. A message event handler that doesn't check origin is a security trap — any page on the web can postMessage to your window.