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

Four small windows: drawer, a panel on the right; floating, a window in the corner; island, a message bar at the bottom; inline, the chat fills a container in your layout

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 showExecutionMode is 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().

↑ ↓ select · Enter open · Escape close