Skip to main content
The chat embed is a self-contained IIFE bundle that drops the Waniwani chat into any website. It talks directly to app.waniwani.ai, so no backend proxy is required on your side.
Platform feature. The dashboard generates the embed snippet for your project. Open app.waniwani.ai to get yours, or read more about the Platform.

Quick start

  1. Open your project on app.waniwani.ai and copy the chat embed snippet from the project settings.
  2. Paste it into your site’s HTML, alongside a mount point where you want the chat to appear:
That’s it. The chat mounts inside your marker div via Shadow DOM (so its styles never leak into your page). The snippet carries identifiers only — your project token, the channel ID, and the API URL. Title, welcome message, placeholder, suggestions, thread history and tool-call display are never baked in: the embed fetches them from your channel at runtime, so changing them in the dashboard updates live sites with no redeploy. Theming is the exception — it has no dashboard equivalent and is set on the tag (data-theme) or in CSS. Don’t hand-author the snippet — the channel ID is issued by the Platform and required for the embed to route correctly.

Modes

The embed renders in one of four modes, set with data-mode on the <script> tag: Use inline when the chat is the page section. Use floating when it should be available everywhere without taking up layout space. Use composer for a search-box entry point inside your own layout (a hero, a help-center header, a row under a pricing table), where a full chat panel takes too much room and a corner bubble is too easy to miss. Use off when you want the tag’s non-chat behavior on a page that should look untouched: publishing tools to browsing agents, or host-page tracking, with nothing on screen.
In composer mode the panel is appended to <body> rather than nested in your marker, so a transform or filter on an ancestor of the marker can’t drag the fixed panel out of position. Give the marker whatever width and margins your layout needs; data-height and the 500px inline default don’t apply. The conversation survives closing the panel: clicking the box again reopens it with the history intact. If you want React-component-level control over the inline mount, see Chat in React.

Sizing

Inline mode only. In floating mode the panel sizes itself, and in composer mode the box is content-height inside a container you size. The embed fills its container. Give the <div data-waniwani-embed> a definite size via CSS, or set data-height on the script tag:
A container you leave unstyled is 500px tall. That default is written with :where(), so it carries no specificity and any rule you write beats it — including height: auto to let the chat grow with its content. A ResizeObserver mirrors the container’s max-height onto the embed so padding and borders are respected.

Overriding attributes

You can edit data-* attributes on the <script> tag to tweak behavior. Do it sparingly: an attribute set here permanently outranks the channel config the embed fetches from the dashboard (resolution order is dashboard → data-* → init()), so a title pinned on the tag can no longer be changed by whoever owns the channel. Floating and composer modes: Composer mode only. The box sits in your layout, so its chrome is adjustable:

Composer variants

Every variant behaves the same: the first message opens the panel. Only the chrome differs. Two, because a variant is a starting point rather than the ceiling. Radius, fill, border, shadow and the send button are CSS custom properties, so a fully rounded search bar is one declaration rather than a third variant. See Restyling with CSS.

Trigger instead of an input

data-composer-trigger="true" renders the box as a tap target: nothing is typed in the page, and a click opens the panel where the caret lands. For a host that wants one tap into the full chat and no in-page composing. The box keeps its variant and whatever CSS you set on it — only the textarea is swapped for the trigger. It is the same swap On mobile applies automatically below 640px; this option applies it at every width.

Restyling with CSS

The box reads its fill, border, radius, shadow and send button from --ww-composer-* custom properties. Set them wherever you set the rest of the widget’s variables ([data-waniwani-embed] works, and so does :root); they inherit through the widget’s Shadow DOM. Anything you leave alone keeps the variant’s own value. A flat box with a single rule under it:
A fully rounded search bar:
A tinted box with a branded send button:
Text color, placeholder color, and the font come from the widget-wide variables in Theming & customization, not from a composer-specific one.

On mobile

Below 640px the composer renders as a tap target rather than a live input, whatever variant you picked. One tap opens the panel, which is already a full-screen sheet at that width, and the visitor types there. This is the default because in-page typing on a phone is the worse half of the handoff: the soft keyboard covers the box, the first message opens a full-screen panel over it, and focus has to move mid-sentence. Going straight to the panel skips all of it. The box keeps its variant and whatever CSS you set on it; only the textarea is swapped out. Set data-composer-mobile-trigger="false" to keep a real input at every width. Programmatic callers pass the same options as an object:
Per-URL show/hide rules are configured in the dashboard, not on the script tag. They gate every mode; in inline and composer mode the marker div collapses too, so a hidden page shows no empty box. In off mode there is nothing to gate. Leave the wiring attributes (data-token, data-channel-id, data-api, the script src) as the dashboard generated them. Beyond the data-theme preset and data-assistant-bubble, individual colors, fonts, and bubble shapes have no data-* equivalent — set --ww-* CSS variables instead. They inherit through the widget’s Shadow DOM, so declare them on [data-waniwani-embed] in inline or composer mode, or anywhere on the host page (:root works) in floating mode, where there is no container of your own. See Theming & customization for the full variable list and deep-customization recipes. Full type definitions: EmbedConfig and ChatTheme.

Programmatic init

For runtime control (computing options dynamically, mounting on demand), drop the data-token attribute from the script tag and call init() manually. You still need the wiring attributes from a dashboard-issued snippet:
init() returns an instance with destroy() and sendMessage(text). You can also call window.WaniWani.chat.destroy() and window.WaniWani.chat.sendMessage("...") directly on the global. init() also accepts an onEvent callback that mirrors chat lifecycle events (opens, messages, errors, link clicks) into your own analytics — Amplitude, Segment, gtag — with your page’s identity attached automatically. See Widget events (onEvent). By default the embed mints its own anonymous visitor id, a stable opaque value persisted in the browser’s localStorage. It rides on every chat request and tracking event, so a visit is attributable before the first message mints a session (see Sessions). If your site already tracks visitors with PostHog, Amplitude, Segment, or a first-party cookie, override that id with your own. Waniwani then correlates its sessions and events to the same visitor you see in your analytics, and your server-side MCP tools and flows read the id back as context.waniwani.visitorId, so they can send events straight to the same analytics tool the id came from. There are three ways to set it. Pick the one that matches when your id is available.

If you know the id up front

Set it declaratively on the script tag:
Or pass it to init() when you initialize programmatically. init() accepts a string or a resolver (sync or async), so you can read the id inline:

If the id resolves asynchronously (the common case)

Analytics SDKs usually assign a distinct id only after they bootstrap, so read it when it’s ready and hand it to the widget. setVisitorId() is safe to call at any time, before or after init(), and the new id applies to the next chat request and tracking event:
A blank value is ignored, so an id that isn’t ready yet never wipes a good one. Read the id the widget will send with window.WaniWani.chat.getVisitorId().
The visitor id identifies a device / browser, not a signed-in account. When a visitor logs in, keep the visitor id and additionally call identify() with your account id, so anonymous and known activity stitch together.

Self-hosting the JS bundle

If you’d rather not load from a CDN, pin a specific SDK version and serve the bundled file yourself. The data-* attributes still come from the dashboard snippet — you’re only swapping where embed.js is served from:
Then point the dashboard-issued snippet at your local copy:

How it works

  1. The script reads its own data-* attributes (token, channel ID, mode, theming, content overrides).
  2. On DOMContentLoaded (or immediately, if the DOM is already ready), it mounts the chat via Shadow DOM — inside [data-waniwani-embed] in inline and composer mode, or in its own anchored container in floating mode. composer mode adds a second, <body>-level container for the panel.
  3. The chat calls app.waniwani.ai/api/mcp/chat with the project token and streams responses back through Server-Sent Events.
  4. Conversation history lives only in memory by default; set data-enable-thread-history to persist threads in IndexedDB on the user’s device.
No server-side integration on your side.