Embed the chat
Add ShappyChat to your web app with the SDK. The SDK supplies the chat UI. A runtime does the work. See Runtimes.
Get the SDK files
The SDK package is @shappychat/chat. It is not on npm. Your Shappy Backend supplies it. In the folder of your app, run this command:
npm install https://chat.example.com/sdk/shappychat.tgz
Use the public URL of your backend. Step 6 of the backend setup shows this command with your URL. The release also has this file as shappychat-<version>.tgz.
If you use TypeScript, set "skipLibCheck": true in your tsconfig.json. The SDK types refer to a package that the private file does not include.
To build the SDK from the source checkout:
./app.sh build
The build writes the SDK to sdk-dist/. Link that folder in your project, or serve these files with your app:
| File | Contents |
|---|---|
sdk-dist/embed.js |
The browser entry point |
sdk-dist/style.css |
The chat styles. They apply only inside the chat, so they do not change the styles of your page. |
sdk-dist/node.js |
The Node entry point (@shappychat/chat/node) |
Mount the chat panel
Use mountShappyChatDrawer. It supplies the full chat surface with the ShappyChat header and launchers.
import { mountShappyChatDrawer, RemoteShappyChatRuntime } from '@shappychat/chat';
import '@shappychat/chat/style.css';
const runtime = await RemoteShappyChatRuntime.connect({
url: backendUrl,
token: appCredential,
conversationId,
});
const drawer = mountShappyChatDrawer(document.body, {
runtime,
presentation: 'drawer',
trigger: chatButton,
onOpenChange: (open) => chatButton.setAttribute('aria-expanded', String(open)),
});
chatButton.addEventListener('click', drawer.toggle);
Warning: Do not put an app credential in a public bundle. Get it from your server for the signed-in user.
To mount only the conversation in a container that you own, use mountShappyChat(container, { runtime }).
Choose a presentation
| Value | Closed | Open |
|---|---|---|
drawer (default) |
Hidden | A panel on the right side of the window |
floating |
A round button in the bottom-right corner | A floating chat window |
island |
A round button at the bottom centre | A message bar. The conversation shows above it when it has messages. |
inline |
Hidden | The chat fills a container in your layout |
In the island, the ghost button folds the conversation. While the conversation is folded, a new message does not open it. Above the message bar, the latest status and the full reply show. A long reply scrolls in that area. Click it, or its chevron, to open the conversation. When the island is closed, its ghost button has no background and half opacity. Thus it does not hide the content of the page.
Change the presentation at any time. The conversation and the unsent text stay.
drawer.setPresentation('island');
Control the drawer
| Method | Description |
|---|---|
show(), hide(), toggle() |
Open or close the chat. Closing does not stop a running reply. |
isOpen() |
Returns true if the chat is open. |
getDraft(), setDraft(text) |
Read or set the unsent message. |
setRuntime(runtime) |
Show another conversation in the same panel. |
setPresentation(value) |
Change the presentation. |
setTheme(theme) |
Change the colours. |
unmount() |
Remove the UI. The runtime continues. |
Change the colours
The default theme is ShappyChat purple, in light and dark mode. Set colours with the theme option or with setTheme.
const drawer = mountShappyChatDrawer(document.body, {
runtime,
theme: { accent: '#5ed6b4', glow: false },
});
You can also set CSS custom properties on an ancestor element:
.my-app {
--shappychat-accent: #5ed6b4;
--shappychat-panel: #f7f7f2;
--shappychat-field: #e9e9e1;
--shappychat-ink: #1f2420;
--shappychat-ghost: #1f2420;
}
| Property | Changes |
|---|---|
--shappychat-accent |
Buttons, links and highlights |
--shappychat-panel |
The chat background |
--shappychat-field |
The message field and controls |
--shappychat-ink |
The text colour |
--shappychat-ghost |
The fill of the ghost mark |
The chat uses dark mode when an ancestor element has the dark class.
Fit the chat to your layout
| Property | Default | Description |
|---|---|---|
--shappychat-top |
0px |
Set this to the height of your header. The panel starts below it. |
--shappychat-width |
440px |
The width of the drawer panel |
Your app controls its own routing and layout. For example, when the drawer opens, you can add space on the right side of your content.
Drawer options
| Option | Description |
|---|---|
runtime |
The runtime of the conversation. Required. |
presentation |
drawer, floating, island or inline |
initialOpen |
Open the chat when it mounts. Default false. |
trigger |
Your chat button. Focus goes back to it when the chat closes. |
intro |
An element that shows above the conversation |
showHeader |
Set false if your app supplies its own toolbar. |
theme |
Colour overrides |
onOpenChange(open) |
Called when the chat opens or closes |
onNewChat() |
Called when the user starts a new chat |
onModelSelect(anchor) |
Called when the user selects the model button |
onError(error) |
Called when an error occurs |
shortcut |
The key that your app binds to open the chat, shown in the island and the launcher. Default { label: '⌘/Ctrl K', title: 'Command/Ctrl K' }. The SDK shows the key. Your app must handle it. |
enableAttachments |
Allow file attachments. Default true. The chat sends images only to a model that reads images. |
showExecutionMode |
Show the Auto, UI and MCP menu. Default false; the chat uses Auto. |
Keyboard
- Enter sends the message. Shift + Enter adds a new line.
- Escape closes the chat. If a menu is open, Escape closes the menu first.
- Shift + Tab in the message field changes the execution mode when
showExecutionModeis on. See App tools (MCP).
Let users choose a model
Use createShappyChatModelPicker. It opens a menu beside the model button, in the width of the chat.
import { createShappyChatModelPicker, mountShappyChatDrawer } from '@shappychat/chat';
const picker = createShappyChatModelPicker({
listModels: () => loadModelsFromYourServer(),
onError: showError,
});
const drawer = mountShappyChatDrawer(document.body, {
runtime,
onModelSelect: (anchor) => anchor && picker.open(anchor, runtime),
});
Close the picker before you replace or remove the runtime: picker.close().