Embedding the launcher

ChatLauncher is a round button in a corner of the viewport that opens a chat panel. Use it directly when the page is your React app. Use mountChatLauncher when the widget goes on a page you do not control: it renders the launcher in a shadow root with its own Mantine provider, so the page styles do not reach the widget and the widget styles do not leak out.

In a React app

Render ChatLauncher anywhere inside your MantineProvider and pass the chat as children. Give AgentChat contentWidth="100%" and wrapLines: the panel is 380px wide by default, and long code lines should wrap instead of scrolling sideways.

import { useState } from "react";
import { AgentChat, ChatLauncher, type ChatMessage } from "@sinups/ai-kit";

export function SupportWidget() {
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [unread, setUnread] = useState(0);

  return (
    <ChatLauncher
      title="Assistant"
      unreadCount={unread}
      onOpenedChange={(opened) => opened && setUnread(0)}
    >
      <AgentChat
        messages={messages}
        status="ready"
        onSend={({ content }) => sendToAgent(content, setMessages)}
        onStop={stopAgent}
        contentWidth="100%"
        wrapLines
        alignComposer
        emptyState={{
          title: "How can I help?",
          description: "Ask about your account, billing or the API.",
        }}
      />
    </ChatLauncher>
  );
}

The open state is uncontrolled by default. Control it with opened and onOpenedChange to open the panel from your own button:

const [opened, setOpened] = useState(false);

<Button onClick={() => setOpened(true)}>Ask the assistant</Button>
<ChatLauncher opened={opened} onOpenedChange={setOpened} position="bottom-left" offset={{ x: 16, y: 88 }}>
  <AgentChat {...chat} contentWidth="100%" wrapLines />
</ChatLauncher>

Behavior

  • The panel is a non-modal dialog. Opening it moves focus into the chat composer; closing it with the header button or Escape returns focus to the launcher button.
  • keepMounted is on by default: the chat keeps its messages, scroll position and draft while the panel is closed.
  • The panel never exceeds the viewport. When it does not fit, the offset drops to 12px. With mobileFullScreen (on by default) the panel opens full screen below fullScreenBreakpoint (520px) and the page behind it stops scrolling.
  • Scrolling the feed to its end does not scroll the host page.
  • unreadCount shows a badge on the closed button and adds the count to its accessible name. labels translates the accessible names.
  • withinPortal is on by default. Turn it off to place the launcher inside a positioned container, for example a preview frame.

On any page

mountChatLauncher(target, element, options) attaches an open shadow root to target (or uses the shadow root you pass), creates a React root with a MantineProvider inside it and renders element. Portals of the widget (menus, popovers, modals) render inside the same shadow root. It returns container and unmount().

import mantineCss from "@mantine/core/styles.css?inline";
import baseCss from "@sinups/ai-kit/styles/base.css?inline";
import launcherCss from "@sinups/ai-kit/styles/ChatLauncher.css?inline";
import chatCss from "@sinups/ai-kit/styles/AgentChat.css?inline";
import providerCss from "@sinups/ai-kit/styles/AiKitProvider.css?inline";
import { AgentChat, AiKitProvider, ChatLauncher, mountChatLauncher } from "@sinups/ai-kit";

const host = document.createElement("div");
document.body.append(host);

const widget = mountChatLauncher(
  host,
  <ChatLauncher title="Assistant">
    <AgentChat {...chat} contentWidth="100%" wrapLines alignComposer />
  </ChatLauncher>,
  {
    styles: [mantineCss, baseCss, launcherCss, chatCss, providerCss],
    theme: { fontFamily: "system-ui, sans-serif" },
    colorScheme: "light",
    wrap: (element) => <AiKitProvider accent="indigo">{element}</AiKitProvider>,
  },
);

// Later, for example when the host page navigates away
widget.unmount();

Styles in the shadow root

Page stylesheets do not apply inside a shadow root, so the widget needs the Mantine and kit stylesheets passed explicitly. Choose one of three ways:

  • styles: CSS text, for example from a bundler import with ?inline (shown above). Pass styles/base.css and the files of the components the widget renders (ChatLauncher, AgentChat, AiKitProvider when you wrap with it) instead of the whole styles.css. :root, html and body selectors are rewritten to the widget container, so Mantine variables are declared on the widget and not on the page.
  • styleUrls: stylesheet URLs linked as they are, without rewriting. Host copies of @mantine/core/styles.css and @sinups/ai-kit/styles.css next to your widget bundle.
  • adoptDocumentStyles: copies the <style> and stylesheet links of the current document head. Use it in development, where CSS modules are injected at runtime.
mountChatLauncher(host, <SupportLauncher />, {
  styleUrls: [
    "https://cdn.example.com/widget/mantine-core.css",
    "https://cdn.example.com/widget/ai-kit.css",
  ],
});
mountChatLauncher(host, <SupportLauncher />, {
  adoptDocumentStyles: import.meta.env.DEV,
  styles: import.meta.env.DEV ? [] : [mantineCss, baseCss, launcherCss, chatCss],
});

Mount options

Option
Default
Description
shadow
true
Render in an open shadow root. With false the widget renders into target directly and page styles apply.
styles
[]
CSS text added to the shadow root, document-level selectors scoped to the widget.
styleUrls
[]
Stylesheet URLs linked in the shadow root without scoping.
adoptDocumentStyles
false
Copy stylesheets from the current document head.
theme
none
MantineThemeOverride of the widget provider.
colorScheme
"light"
Forced color scheme of the widget: light or dark.
wrap
none
Wraps the element inside the provider, for example with AiKitProvider.

Checklist

  • Both stylesheets reach the shadow root: Mantine first, then the kit.
  • The widget has its own font in theme.fontFamily: inherited page fonts can differ from what you tested.
  • unmount() is called when the host page removes the widget; it also removes the added style nodes.
  • Check the panel at 390px viewport width: it opens full screen there.