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.
keepMountedis 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 belowfullScreenBreakpoint(520px) and the page behind it stops scrolling. - Scrolling the feed to its end does not scroll the host page.
unreadCountshows a badge on the closed button and adds the count to its accessible name.labelstranslates the accessible names.withinPortalis 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). Passstyles/base.cssand the files of the components the widget renders (ChatLauncher,AgentChat,AiKitProviderwhen you wrap with it) instead of the wholestyles.css.:root,htmlandbodyselectors 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.cssand@sinups/ai-kit/styles.cssnext 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
shadowtruestyles[]styleUrls[]adoptDocumentStylesfalsethemecolorScheme"light"wrapChecklist
- 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.