Widget builder

The embed build carries no React and no dependencies: under 10 KB gzip, its own shadow root, and the chat itself opens in an iframe on the address you give it. Pick the options below, copy the snippet and paste it before </body>.

Builder

Where the widget is shown

Button

Icon

Placement

Corner

Opening

0 keeps it closed

Animation

Second face of the button

Plays on the closed button; with an avatar it alternates the two faces

Actions

9 max

Code to paste

<script src="https://cdn.jsdelivr.net/npm/@sinups/ai-kit/dist/embed/widget.js" defer></script>
<script>
  window.addEventListener("load", () => {
    window.aiKitChat = AiKitChat({
      "url": "https://chat.example.com/widget",
      "position": "bottom-right",
      "offset": 30,
      "color": "rgb(17, 17, 17)",
      "size": "medium",
      "icon": "chat",
      "title": "Assistant",
      "iconAnimation": "coin",
      "pulse": true,
      "actions": [
        {
          "id": "chat",
          "label": "Ответим сразу же",
          "icon": "<svg fill=\"currentColor\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M1.53467 17.8613C0.561333 16.128 0 14.1307 0 12C0 5.372 5.372 0 12 0C18.628 0 24 5.372 24 12C24 18.628 18.628 24 12 24C9.86933 24 7.872 23.4387 6.13867 22.4653L0 24L1.53467 17.8613Z\" fill=\"currentColor\"/></svg>",
          "opensChat": true
        },
        {
          "id": "callback",
          "label": "Перезвоним через 10 секунд",
          "icon": "<svg fill=\"currentColor\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M10.3416 13.659C8.64498 11.9625 7.36597 10.0629 6.51619 8.1358C6.33638 7.72834 6.44224 7.25128 6.75691 6.93662L7.94457 5.75049C8.9176 4.77751 8.9176 3.40143 8.06783 2.5517L6.36538 0.84936C5.23283 -0.28312 3.39697 -0.28312 2.26442 0.84936L1.31894 1.79479C0.244399 2.86926 -0.20369 4.41935 0.0863353 5.95639C0.802697 9.74534 3.00399 13.8939 6.55535 17.445C10.1067 20.9962 14.2555 23.1974 18.0447 23.9137C19.5818 24.2037 21.132 23.7556 22.2066 22.6811L23.1506 21.7372C24.2831 20.6047 24.2831 18.7689 23.1506 17.6365L21.4496 15.9356C20.5998 15.0858 19.2222 15.0858 18.3739 15.9356L17.0644 17.2464C16.7497 17.5611 16.2726 17.6669 15.8652 17.4871C13.9379 16.6359 12.0383 15.3555 10.3416 13.659Z\" fill=\"currentColor\"/></svg>",
          "href": "tel:+10000000"
        }
      ]
    });
  });
</script>

Preview

The panel opens empty here: the chat address is only used on your own page

Brand marks in the presets belong to their owners; the package itself ships none.

Install

Everything can be written straight into the tag: a data-* attribute per option, with data-actions and data-labels taking JSON. No JavaScript is needed.

<script src="https://cdn.jsdelivr.net/npm/@sinups/ai-kit/dist/embed/widget.js" defer
  data-url="https://chat.example.com/widget"
  data-title="Assistant"
  data-color="#0a84ff"
  data-size="medium"
  data-position="bottom-right"
  data-offset="30"
  data-pulse="true"
  data-open-after="15000"
  data-devices="all"
  data-actions='[
    {"id":"chat","label":"Chat with us","opensChat":true},
    {"id":"call","label":"Call me back","href":"tel:+10000000"},
    {"id":"telegram","label":"Telegram","href":"https://t.me/example","color":"#2aabee"}
  ]'></script>

The same thing as a call, when the page wants to keep the widget and drive it later.AiKitChat returns the widget; the auto-started one also lands on window.aiKitChat.

<script src="https://cdn.jsdelivr.net/npm/@sinups/ai-kit/dist/embed/widget.js" defer></script>
<script>
  window.addEventListener("load", () => {
    window.chat = AiKitChat({
      url: "https://chat.example.com/widget",
      title: "Assistant",
      color: "#0a84ff",
      location: ["bottom", "right"],
      pulse: true,
      openAfter: 15000,
      labels: { open: "Открыть чат", actions: "Как с нами связаться" },
      actions: [
        { id: "chat", label: "Чат с ассистентом", opensChat: true },
        { id: "call", label: "Перезвоним за 10 секунд", href: "tel:+10000000" },
      ],
    });
  });
</script>

A remote JSON is for those who want to change the settings without touching the page — an admin panel, for example. It is optional: data-config-url (or data-app-id with data-config-endpoint) is fetched and merged, and what the tag sets stays above it.

<script src="https://cdn.jsdelivr.net/npm/@sinups/ai-kit/dist/embed/widget.js" defer
  data-url="https://chat.example.com/widget"
  data-config-url="https://cdn.example.com/widget.json"></script>

In a bundler the same widget comes from @sinups/ai-kit/embed, with the full control surface in return.

import { chatWidget } from "@sinups/ai-kit/embed";

const widget = chatWidget({
  url: "https://chat.example.com/widget",
  position: "bottom-right",
  color: "rgb(10, 132, 255)",
  openAfter: 15000,
  actions: [
    { id: "chat", label: "Chat with us", opensChat: true },
    { id: "call", label: "Call me back", href: "tel:+10000000" },
  ],
});

widget.notify({
  title: "Need a hand?",
  text: "We answer in a minute and keep the thread in your inbox",
  avatar: "https://example.com/ada.png",
  timeout: false,
});

Options

Option
Default
Description
url
none
Address of the chat page shown in the panel.
configUrl
none
JSON with the same options, fetched and merged under the ones set here. appId with configEndpoint works the same way.
position
bottom-right
Corner: bottom-right, bottom-left, top-right, top-left.
offset
30
Distance from the edges in px, a number or { x, y }.
location
none
[vertical, horizontal] in one go: words pick the edge (top, bottom, left, right), numbers also set the distance, and a negative number counts from the opposite edge.
color
blue
Accent of the button and of the actions without their own color.
size
medium
small, medium, large or a diameter in px; the spacing of the fan follows it.
icon
chat
chat, circle, dots, send, inline SVG or an image URL.
avatar
none
Second face of the button, shown by iconAnimation.
iconAnimation
none
none, circle, coin, flip: with an avatar the two faces alternate, without one the icon itself turns or grows.
pulse
false
Slow rings around the closed button.
ringColor
color
Color of the rings and of the halo around the open button.
openAfter
0
Opens the panel after this many milliseconds.
openAfterOnMobile
false
Whether the timer also works on a narrow screen.
remember
true
Skips the timer once the visitor closed the widget.
devices
all
all, desktop or mobile.
mobileHeight
100
Height of the panel on a narrow screen, in percent.
actions
none
Up to nine buttons: id, label, icon, color, href or opensChat, unread.
actionsMotion
sequence
sequence sends them out one after another, together almost at once.
notifications
true
Shows the bubbles of notify.
indicator
true
Shows the unread count on the button.
notificationTimeout
12000
How long a bubble lives when the call does not say otherwise.
notificationLimit
3
How many bubbles stay on screen at once; the oldest leaves first.
theme
auto
light, dark or auto, which follows the visitor.
iconColor
picked
Color of the icon on the button; picked from the background when it is a hex.
panelWidth
380
Panel width in px, never wider than the viewport.
panelHeight
640
Panel height in px, never taller than the space above the button.
actionSize
size
Diameter of an action button; follows the button size until it is set.
actionGap
size / 6
Space between the action buttons.
mobileBreakpoint
520
Width below which the panel opens full screen.
zIndex
2147483000
Stacking order of the widget.
labels
English
Every string of the widget: open, close, actions, closeActions, panel, unread, dismissNotification.
css
none
CSS added inside the shadow root. Keep it out of a remote config you do not control.
defer
true
Loads the chat page on the first open.

Notifications

A bubble takes a string or an object with title, content (or text), avatar and timeout. Up to notificationLimit of them stay on screen, newest next to the button; each has its own timer and its own hide. A click on the bubble opens the chat, the cross closes that bubble, and an avatar that fails to load is dropped instead of showing a broken image.

// short form
widget.notify("Hi! Need a hand with the order?");

// full form: title, avatar, its own lifetime
widget.notify({
  title: "Anna from support",
  text: "Delivery across the EU takes 3 to 5 working days. Shall I cover returns?",
  avatar: "https://example.com/anna.png",
  timeout: 15000,        // ms; false keeps it until it is dismissed
});

// the call returns the hide of that one bubble
const { hide } = widget.notify({ text: "Stays until you say so", timeout: false });
setTimeout(hide, 3000);

Ways pages use it, all built from the same few calls:

const chat = window.aiKitChat;

// a greeting after the visitor has read for a while
setTimeout(() => chat.notify({ title: "Anna from support", text: "Anything I can help with?" }), 20000);

// a hint when the visitor is about to leave
document.addEventListener("mouseleave", (event) => {
  if (event.clientY <= 0) {
    chat.notify({ text: "Leaving? I can send the quote by email", id: "exit", timeout: false });
  }
}, { once: true });

// after the visitor scrolled through the pricing
const pricing = document.querySelector("#pricing");
new IntersectionObserver(([entry], observer) => {
  if (entry.isIntersecting) {
    chat.notify({ text: "Compare the plans with me?", id: "pricing" });
    observer.disconnect();
  }
}).observe(pricing);

// own button on the page instead of the launcher
document.querySelector("#ask").addEventListener("click", () => chat.open());

// a question about the current page
document.querySelector("#ask-about-order").addEventListener("click", () => {
  chat.navigate("https://chat.example.com/widget?order=A-1024");
});

// count of unanswered replies, kept by the page
chat.unread(3);
chat.on("open", () => chat.unread(0));

// what the visitor picked in the fan
chat.on("action", (id) => analytics.track("widget_action", { id }));

// quieter on a landing page, louder in the account
chat.setOptions({ pulse: false, openAfter: 0 });
chat.options.color = "#0a84ff";

// take it off a page where it does not belong
if (location.pathname.startsWith("/checkout")) {
  chat.hide();
}

Control from the page

  • open, close, toggle — the panel.
  • notify — a bubble above the closed button with a title, an avatar and its own lifetime; timeout: false keeps it until it is dismissed, and the call returns hide for that one bubble. The text takes **bold**, `code` and [links](https://example.com); everything else is escaped.
  • clearNotifications — takes every bubble off the screen.
  • unread — the count on the button, cleared when the chat opens.
  • setOptions — new options at any time; the live options object takes a single field, as in widget.options.color = "#000".
  • hide and show — take the widget off the page and bring it back.
  • navigate — open the panel on another address.
  • on — events of the widget (open, close, action, message, ready); send — a message into the chat page.
  • destroy — remove the widget with its listeners and timers.

Messages with the chat

The page in the panel talks to the widget through postMessage. A message is taken only when it comes from the frame of the widget itself and from the origin of url, so neither another tab nor another frame of the same site can close the widget or push a notification into it. Send with the origin of the host page, not "*".

// inside the chat page, in the iframe
const host = "https://shop.example.com";  // the page the widget runs on
parent.postMessage({ source: "ai-kit-chat", type: "unread", count: 3 }, host);
parent.postMessage({ source: "ai-kit-chat", type: "notify", title: "New reply", text: "Your ticket is answered" }, host);
parent.postMessage({ source: "ai-kit-chat", type: "close" }, host);

// on the page around it
widget.on("action", (id) => analytics.track("widget_action", { id }));
widget.send("locale", { locale: "ru" });