Developers
Widget JavaScript API
Install the Sela chat widget with one script tag, configure it with data attributes, and control it from your page with window.SelaWidget.
Last updated: 2026-09-29
The Sela widget is a small script (widget.js) that adds a launcher button to your page and opens the chat in an iframe hosted by Sela. This page covers the install attributes, the window.SelaWidget API, the analytics hook, framework examples and what your Content Security Policy must allow. If you are a business owner setting up the widget from the dashboard, read Web chat widget first.
Install with a script tag#
Add this snippet to every page where the widget should appear. Replace org_XXXXXXXX with your organization ID (shown in the dashboard under Integrations, in the Website Widget section).
<script
src="https://usesela.com/widget.js"
data-organization-id="org_XXXXXXXX"
data-position="bottom-right"
data-theme="auto"
data-primary-color="#0f3fcc"
data-button-size="medium"
data-widget-icon="default"
data-z-index="999999"
data-hide-on-mobile="false"
async
></script>The script initializes itself when it loads. There is no separate init call. Both async and a normal blocking script work, and it can sit in <head> or before </body>.
The script must be a classic <script> tag with the attributes on the tag itself. It reads its configuration from document.currentScript, so a type="module" script or a script injected without the attributes will not configure correctly.
Script attributes#
| Attribute | Values | Default | Notes |
|---|---|---|---|
data-organization-id | Your organization ID | Required | Must match ^[a-zA-Z0-9][a-zA-Z0-9_-]{1,127}$. A missing or invalid value disables the widget silently. |
data-position | bottom-right, bottom-left, top-right, top-left | bottom-right | The launcher sits 20 px from the edges. The panel opens 90 px from the edge. |
data-theme | light, dark, auto | auto | auto follows the visitor's prefers-color-scheme and updates live. |
data-primary-color | #rgb or #rrggbb | #3b82f6 | Colors the launcher and is passed to the chat panel for accents. Any other format silently falls back to the default. |
data-button-size | small (50 px), medium (60 px), large (70 px) | medium | |
data-widget-icon | default, headset, robot, sparkles, help-circle | default | |
data-z-index | Integer from 1 to 2147483647 | 999999 | The chat panel uses this value minus 1. |
data-hide-on-mobile | true | Off | Checked once when the widget renders: hides it when the window is 768 px wide or narrower. |
Invalid values for the enumerated attributes revert to the default without an error.
Identity attributes such as data-email or data-user-hash do not exist. Identity verification does not work through the script tag; see Identity verification (HMAC).
What the script creates#
- A launcher
<button id="Sela-widget-button">. - A chat panel
<div id="Sela-widget-container" role="dialog">, 400 by 600 px on desktop (capped at the viewport size), and nearly full width on phones. - An iframe pointing at
https://widget.usesela.com/withorgId,themeandprimaryColoras query parameters. The iframe is allowed to use the microphone and clipboard.
The iframe loads lazily. Its src is set the first time the panel opens, or when the visitor hovers or focuses the launcher. The script also adds preconnect and dns-prefetch hints for the widget origin so the first open feels fast.
The window.SelaWidget API#
The script exposes one global, window.SelaWidget. There is no window.Sela alias; code that calls window.Sela fails.
| Method | What it does |
|---|---|
show() | Opens the chat panel. |
hide() | Closes the chat panel. |
isOpen() | Returns true while the panel is open. |
updateConfig(partial) | Merges and sanitizes the new options. Only primaryColor, widgetIcon and theme change the page live. |
init(partial) | Destroys the widget and rebuilds it with the merged options. Use this for a full refresh. |
destroy() | Removes the launcher and panel and stops listening for messages. |
getConfig() | Returns a copy of the current config. |
updateConfig and init take camelCase keys:
| Key | Values |
|---|---|
orgId | Organization ID |
position | bottom-right, bottom-left, top-right, top-left |
theme | light, dark, auto |
primaryColor | #rgb or #rrggbb |
buttonSize | small, medium, large |
widgetIcon | default, headset, robot, sparkles, help-circle |
zIndex | Integer, 1 to 2147483647 |
hideOnMobile | Boolean |
// Open the chat from your own "Contact us" button
document.querySelector("#contact-us").addEventListener("click", () => {
window.SelaWidget?.show();
});
// Switch the icon and color live
window.SelaWidget?.updateConfig({ widgetIcon: "headset", primaryColor: "#0f3fcc" });
// Move the launcher: position changes need a rebuild
window.SelaWidget?.init({ position: "bottom-left" });
console.log(window.SelaWidget?.getConfig());
// { orgId, position, theme, primaryColor, buttonSize, widgetIcon, zIndex, hideOnMobile }Timing rules#
show()andhide()do nothing until the widget has rendered, which happens afterDOMContentLoaded. Guard calls withwindow.SelaWidget?.and, if you call very early, wait forDOMContentLoaded.- With
data-hide-on-mobile="true"on a narrow screen nothing is ever created, soshow()is a no-op there.
Events and analytics#
The widget does not fire DOM events and does not postMessage to your page. The one host-facing hook is a global analytics object. If window.selaAnalytics.track is a function, the script calls it as track(eventName, props). You can define it before or after the script loads.
<script>
window.selaAnalytics = {
track(name, props) {
// Forward to your analytics tool
console.log(name, props);
},
};
</script>| Event | Fired when | Extra properties |
|---|---|---|
widget_initialized | The launcher is rendered | position, theme |
widget_loaded | The chat iframe finished loading | None |
widget_error | The chat iframe failed to load | error: "iframe_load_failed" |
widget_opened | The panel opened | None |
widget_closed | The panel closed | None |
Every call includes orgId and an ISO 8601 timestamp in props.
Framework examples#
Plain HTML#
Use the snippet at the top of this page.
React (Vite or Create React App)#
import { useEffect } from "react";
export function SelaWidget({ organizationId }: { organizationId: string }) {
useEffect(() => {
const script = document.createElement("script");
script.src = "https://usesela.com/widget.js";
script.async = true;
script.setAttribute("data-organization-id", organizationId);
script.setAttribute("data-primary-color", "#0f3fcc");
document.body.appendChild(script);
return () => {
window.SelaWidget?.destroy();
script.remove();
};
}, [organizationId]);
return null;
}Mount it once near the root of your app. The cleanup removes the widget when the component unmounts, which avoids a duplicate launcher in React Strict Mode.
Next.js (App Router)#
import Script from "next/script";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Script
src="https://usesela.com/widget.js"
strategy="afterInteractive"
data-organization-id={process.env.NEXT_PUBLIC_SELA_ORG_ID}
data-primary-color="#0f3fcc"
/>
</body>
</html>
);
}next/script renders the data-* attributes onto the script element, which is what the widget reads.
TypeScript typing#
interface Window {
SelaWidget?: {
init(config?: Record<string, unknown>): void;
show(): void;
hide(): void;
destroy(): void;
updateConfig(config: Record<string, unknown>): void;
getConfig(): Record<string, unknown>;
isOpen(): boolean;
};
selaAnalytics?: { track(name: string, props?: Record<string, unknown>): void };
}Content Security Policy and host page notes#
If your site sends a Content Security Policy, allow Sela's script and iframe:
script-src https://usesela.com;
frame-src https://widget.usesela.com;The widget iframe talks to Sela's backend itself, so your connect-src does not need to change. Other notes:
- The chat runs in a cross-origin iframe, so your page's CSS and JavaScript cannot read or style its content. This keeps visitor conversations isolated from the host page.
- Voice uses the microphone. If you set a
Permissions-Policyheader, do not blockmicrophoneforhttps://widget.usesela.com. - Single-page apps: add the script once. Route changes do not need a re-init.
- Ad blockers or privacy extensions that block
usesela.comalso block the widget. Nothing appears in the console in that case, so test in a clean browser profile. - To hide the launcher on some pages, only include the script on the pages you want, or call
window.SelaWidget.destroy().
Logged-in customers#
Because the script tag cannot carry a signed identity, recognizing logged-in users means loading the hosted widget page in your own iframe or WebView with a server-signed URL. The full recipe, with Node, Python and PHP samples, is in Identity verification (HMAC).
Troubleshooting#
| Symptom | Likely cause |
|---|---|
| Nothing appears and there are no errors | The attribute is data-org-id instead of data-organization-id, the ID is invalid, or the script is type="module". |
window.SelaWidget is undefined | The script has not finished loading, was blocked by CSP or an ad blocker, or exited because of a bad organization ID. |
| Panel opens but stays blank | frame-src (or default-src) in your Content Security Policy blocks https://widget.usesela.com. |
| Launcher hidden on phones | data-hide-on-mobile="true" is set. |
| Position or size does not change | Use init() instead of updateConfig() for those keys. |
| Chat panel shows an organization error | The organization ID does not match a Sela workspace. Copy it again from the dashboard. |
Can't find what you need? Contact support.