@classic-homes/chat-widget
High-performance embeddable chat widget built with Svelte. Can be integrated via multiple methods: Vanilla JS, React, Docusaurus plugin, or Custom Element.
Installation
npm install @classic-homes/chat-widget
For React projects, ensure you have React 17+ installed:
npm install react react-dom
Usage
Vanilla JavaScript
The simplest way to add the widget to any website:
import { init, destroy } from '@classic-homes/chat-widget/vanilla';
// Initialize the widget
init({
apiBase: 'https://api.example.com',
site: 'docs',
siteName: 'Documentation',
title: 'Help Assistant',
welcomeMessage: 'How can I help you today?',
welcomeTopics: ['Getting started', 'API reference', 'Troubleshooting'],
});
// Later, to destroy the widget
destroy();
React Component
Use the React adapter for seamless integration with React applications:
import { ChatWidget } from '@classic-homes/chat-widget/react';
function App() {
return (
<ChatWidget
apiBase="https://api.example.com"
site="docs"
siteName="Documentation"
title="Help Assistant"
welcomeMessage="How can I help you today?"
welcomeTopics={['Getting started', 'API reference', 'Troubleshooting']}
/>
);
}
The React adapter also provides ChatWidgetElement for using the custom element directly:
import { ChatWidgetElement } from '@classic-homes/chat-widget/react';
function App() {
return (
<ChatWidgetElement
apiBase="https://api.example.com"
site="docs"
siteName="Documentation"
/>
);
}
Docusaurus Plugin
For Docusaurus 3.x sites:
// docusaurus.config.js
module.exports = {
plugins: [
[
'@classic-homes/chat-widget/docusaurus',
{
apiBase: 'https://api.example.com',
site: 'docs',
siteName: 'Documentation',
title: 'Help Assistant',
},
],
],
};
Custom Element (Web Component)
Use the <chat-widget> custom element directly in HTML:
<script type="module">
import '@classic-homes/chat-widget';
</script>
<chat-widget
api-base="https://api.example.com"
site="docs"
site-name="Documentation"
title="Help Assistant"
welcome-message="How can I help you today?"
welcome-topics='["Getting started", "API reference"]'
></chat-widget>
Note: Attribute names use kebab-case (e.g., api-base instead of apiBase).
Configuration Options
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
apiBase | string | Yes | - | Base URL for the chat API |
site | string | Yes | - | Site identifier (e.g., "docs", "marketing") |
siteName | string | Yes | - | Display name for current site |
title | string | No | "Chat Assistant" | Widget header title |
logoUrl | string | No | - | Logo URL for header |
primaryColor | string | No | - | Primary brand color (CSS color value) |
position | "bottom-right" | "bottom-left" | No | "bottom-right" | Widget position |
fullscreen | boolean | No | false | Enable fullscreen mode |
welcomeMessage | string | No | - | Welcome message text |
welcomeTopics | string[] | No | - | Suggested topics shown in welcome |
disclaimerHtml | string | No | - | Disclaimer HTML shown below welcome |
storagePrefix | string | No | "chat_" | Prefix for localStorage keys |
sentryDsn | string | No | - | Sentry DSN for error tracking |
environment | "development" | "production" | No | "production" | Environment name |
sites | Record<string, SiteConfig> | No | - | Cross-site configuration |
onNavigate | (url: string) => void | No | - | Custom navigation handler |
onOpen | () => void | No | - | Called when widget opens |
onClose | () => void | No | - | Called when widget closes |
getAuthToken | () => string | null | Promise<string | null> | No | - | CHAPI account bearer token provider (see Account Login) |
Account Login
The widget is anonymous by default — history is scoped to a per-browser anon
token. To scope history to a logged-in CHAPI account instead, the host page owns
login and hands the widget a getAuthToken callback that returns the current
account bearer token (or null when logged out). The widget attaches it as
Authorization: Bearer <token> and, on first authenticated init, claims the
visitor's anon history into the account.
The widget bundles no login UI — the embedding page is responsible for authenticating against CHAPI (it already must, so its origin is whitelisted for SSO).
Programmatic / Vanilla / Docusaurus — pass getAuthToken in the config:
const widget = initChatWidget(element, {
apiBase: 'https://api.example.com',
site: 'docs',
siteName: 'Documentation',
getAuthToken: () => myAuthClient.getAccessToken(), // string | null | Promise<...>
});
React — pass getAuthToken as a prop. Use a stable reference
(useCallback); the token provider is intentionally excluded from the remount deps,
so its identity changing will not remount the widget:
const getAuthToken = useCallback(() => myAuthClient.getAccessToken(), []);
<ChatWidget apiBase={...} site="docs" siteName="Documentation" getAuthToken={getAuthToken} />
Custom element <chat-widget> — a function can't be an HTML attribute, so set it
as a property before the element connects:
<chat-widget id="chat" api-base="https://api.example.com" site="docs" site-name="Documentation"></chat-widget>
<script type="module">
import '@classic-homes/chat-widget';
const el = document.getElementById('chat');
el.getAuthToken = () => myAuthClient.getAccessToken();
</script>
Set the property before the element is added to the DOM (or set it, then re-append) so it's present when the widget mounts.
After login/logout — the account is bound at mount. If the host's auth state
changes while the widget is already mounted, call widget.refresh() to remount and
re-run the account claim with the new token.
Widget API
The initChatWidget function returns a ChatWidgetAPI object with the following methods:
Conversation Management
const widget = initChatWidget(element, config);
// List all conversations
const conversations = await widget.listConversations();
// Get a specific conversation with messages
const conversation = await widget.getConversation(id);
// Switch to a conversation
await widget.selectConversation(id);
// Start a new conversation
await widget.startNewConversation();
// Delete a conversation
await widget.deleteConversation(id);
// Rename a conversation
await widget.renameConversation(id, 'New Title');
State Getters
// Get current conversation ID (null if new chat)
const currentId = await widget.getCurrentConversationId();
// Get current conversation title
const title = await widget.getCurrentTitle();
Lifecycle Methods
// Clear messages and reset chat
widget.clearMessages();
// Refresh widget (reload from storage)
widget.refresh();
// Destroy the widget
widget.destroy();
Events
Subscribe to widget events using the on method:
const widget = initChatWidget(element, config);
// Subscribe to events
const unsubscribe = widget.on('message:received', ({ message }) => {
console.log('New message:', message.content);
});
// One-time subscription
widget.once('ready', () => {
console.log('Widget is ready');
});
// Unsubscribe
unsubscribe();
Available Events
| Event | Payload | Description |
|---|---|---|
ready | undefined | Widget has finished initializing |
conversation:created | { id: string, title: string | null } | New conversation created |
conversation:selected | { id: string | null, title: string | null } | Conversation selected/switched |
conversation:deleted | { id: string } | Conversation deleted |
conversation:titleChanged | { id: string, title: string } | Conversation renamed |
conversations:updated | { conversations: ConversationPreview[] } | Conversation list changed |
message:sent | { message: Message } | User sent a message |
message:received | { message: Message } | Assistant response received |
error | { message: string, error?: Error } | Error occurred |
TypeScript Support
Full TypeScript definitions are included. Import types as needed:
import type {
ChatWidgetConfig,
ChatWidgetAPI,
PageContext,
Message,
ConversationPreview,
} from '@classic-homes/chat-widget';
Styling
The widget is a self-contained Classic Theme island. Its stylesheet (theme
tokens, the --chat-* bridge and the Tailwind utilities theme components need)
is scoped to .chat-widget, so it neither depends on nor leaks into the host
page's CSS. The script injects it automatically before the first render; hosts
may also load @classic-homes/chat-widget/styles.css with a <link> to avoid
a flash before the script runs (loading both is harmless).
The widget ships no web fonts: it uses Figtree when the host page loads it and falls back to the system UI font otherwise.
Brand color: pass primaryColor at init, which sets --chat-primary inline on
the widget root. Other --chat-* variables are declared on .chat-widget; to
override them, target that element with a more specific selector:
body .chat-widget {
--chat-radius: 12px;
--chat-header-bg: #f2f3ee;
}
Browser Support
- Chrome/Edge 88+
- Firefox 78+
- Safari 14+
Decorative tints and shadows use color-mix() (Safari 16.2+, Chrome 111+,
Firefox 113+); older browsers render without them.
The widget requires support for:
- Custom Elements v1
- ES Modules
- CSS Custom Properties
License
MIT