Connectly (opens in a new tab)

Connectly Webchat

A floating launcher and a chat panel you add to any page. The visitor types, the conversation lands in your Connectly inbox. One script tag, or one import.

The whole integration

<script defer src="https://webchat.connectly.ai/webchat2026.min.js"></script>
<script type="module">
  window.ConnectlyWebchat.init({
    clientKey: '<your-client-key>',
  });
</script>

Customize the selected canvas Documentation Open in settings (opens in a new tab)

Install Configure API Troubleshooting ↑ Top

Connectly Webchat

A floating launcher and a chat panel you add to any page. A visitor clicks the launcher, types, and the conversation lands in your Connectly inbox alongside every other channel. There is no page to build and no UI to write.

What you need

One thing: a client key, from the webchat settings page in your Connectly dashboard. Everything else — colours, the launcher's side and icon, the panel's size, the greeting — is stored against that key and fetched at runtime, which is why a working integration is two lines rather than a configuration file.

The key goes in your page's HTML where anyone can read it. That is fine — it names your business, it does not authorise anything.

What it is not

It is not an iframe you size yourself, and it is not a component library. The widget lives in a shadow root and is position: fixed to a viewport corner, so it does not inherit your page's CSS and does not participate in your layout. Where you put the element in the DOM does not matter.

It is also not something you should load twice — see "One integration per page" on the next page. Two integrations means two launchers, two sessions and two sockets for one visitor, and nothing errors.

Install

Two paths, decided by a single question: does your site have an npm install step?

  • No build step — a CMS, a page builder, a static site, a tag manager, anywhere you can paste HTML — use the script tag.
  • Bundling your own frontend — React, Vue, Svelte, Solid, Angular — use the npm package. It ships ES modules and a typed React wrapper.

The script tag

Paste this before the closing </body> tag. <head> also works — the script defers itself until the page is ready either way.

<script defer src="https://webchat.connectly.ai/webchat2026.min.js"></script>
<connectly-webchat client-key="<your-client-key>"></connectly-webchat>

That is the whole integration. The launcher appears in the corner once the widget loads.

Place the element; do not call a function. <connectly-webchat> is a custom element, and the browser upgrades whatever elements are already on the page the moment the definition lands. Your markup may therefore sit before or after the script tag with the same result, and this form works where a CMS, a theme or a tag manager lets you add markup but not run a script.

If you do need the JS API, keep the call in a second tag carrying defer or type="module", after the first. Both attributes hold execution until the document has parsed and then run in document order, so window.ConnectlyWebchat exists by the time the second tag runs; a plain inline <script> with neither attribute runs first, against a global that is not there yet. The sturdier form — and the one to use when you cannot control tag order — is the ready event, which you may subscribe to before the widget exists because it bubbles to document:

<script defer src="https://webchat.connectly.ai/webchat2026.min.js"></script>
<connectly-webchat client-key="<your-client-key>"></connectly-webchat>
<script>
  // A plain inline script is fine HERE: the listener is registered before the deferred
  // bundle runs, which is exactly the ordering problem the ready event solves.
  document.addEventListener('connectly-webchat:ready', (event) => {
    console.log('webchat ready', event.detail);
  });
</script>

open() is deliberately not what this example calls: it would pop the panel up for every visitor on every page. Use the event to hook up analytics, and leave opening to the reader.

The event fires when the launcher renders, and is not replayed — register the listener before the widget loads rather than after. It fires again if the widget is torn down and re-initialized (a changed client-key, or an SPA remount), so it is once per launcher rather than once per page.

The npm package

npm i git+https://github.com/connectlyai/webchat.git#v1.1.0

A git URL rather than a registry name: the package is distributed from its own GitHub repository, which npm, pnpm and yarn all install from natively.

Always pin a tag. A git specifier resolves to a commit, so omit the fragment after # and you get whatever is on the default branch at install time, which moves under you. #semver:^1.1.0 also works, resolving against the repository's tags. If the install asks for credentials, see "npm cannot install the git URL" under Troubleshooting.

ESM only, browser only. There is no CommonJS build, so reach it through import or a bundler, never require().

React

'use client'; // Next.js App Router only — see "Server rendering" below.

import { ConnectlyWebchat } from '@connectly/webchat/react';

export function App() {
  return <ConnectlyWebchat clientKey="<your-client-key>" />;
}

The component renders null. The widget is created imperatively and lives in <body>, outside your tree; unmounting the component ends the session. react and react-dom are optional peer dependencies, needed only for this entry point.

Any other framework, or none

import '@connectly/webchat';

That import is the integration: it defines the <connectly-webchat> custom element and configures the origin. Then put the element on the page.

<connectly-webchat client-key="<your-client-key>"></connectly-webchat>

Svelte, Vue, Solid and Angular all render custom elements natively, so the import plus the tag is everything — with two exceptions that break the build otherwise.

Vue needs isCustomElement, or the compiler treats the tag as a component it cannot resolve, warns on every render, and emits nothing:

vue({ template: { compilerOptions: { isCustomElement: (tag) => tag === 'connectly-webchat' } } })

Angular needs schemas: [CUSTOM_ELEMENTS_SCHEMA] on the component, or the template fails with NG0304.

Where you put the element in the DOM does not matter — it is a 0×0, pointer-events: none mount point, and the launcher is fixed to the viewport corner. One exception: an ancestor with a transform, filter or contain becomes the containing block for position: fixed, so the element relocates itself to <body> — which breaks any framework that thinks it still owns that node. Append to <body> yourself, or use the React wrapper.

Server rendering

Never import the package on the server. It defines a custom element extending HTMLElement, which does not exist in a server runtime, so the import itself throws ReferenceError: HTMLElement is not defined. That includes @connectly/webchat/react, which imports the main entry.

Next.js, both routers — load it through next/dynamic with ssr: false, because even a client component is prerendered during next build:

'use client'; // App Router only; also what allows `ssr: false` here.

import dynamic from 'next/dynamic';

const ConnectlyWebchat = dynamic(
  () => import('@connectly/webchat/react').then((m) => m.ConnectlyWebchat),
  { ssr: false },
);

export function Webchat() {
  return <ConnectlyWebchat clientKey="<your-client-key>" />;
}

Everywhere else — Remix, Nuxt, Astro, SvelteKit — the rule is the same: reach it from a browser-only path (a dynamic import() in a mount or effect hook, a <ClientOnly> wrapper, client:only, a plain module <script>), never a static import at the top of a shared module.

One integration per page

Do not load the script tag and the npm package on the same page.

You get two launchers, two sessions and two sockets for one visitor, and the transcript splits unpredictably between them. Nothing errors and nothing warns — multiple widgets on one page is deliberately supported for other cases — so this is a bug you will only find by looking at it.

Configure

Most of the appearance is not configured in code. Colours, the launcher's side and icon, the panel's size and the greeting are stored against your client key in the Connectly dashboard and fetched at runtime, so the common case needs no config at all — change it in the dashboard and every page using that key follows, with no redeploy. Everything below is for what the dashboard cannot cover: a per-page override, a value driven by your own state, or a theme that has to match the page it sits on.

The precedence chain

Four layers, lowest to highest:

built-in defaults  <  dashboard settings  <  `config`  <  HTML attributes

They merge per group, not wholesale, so { launcher: { side: 'left' } } moves the launcher without resetting its size, icon and offsets to defaults.

HTML attributes get the last word. An attribute is in the page's own markup, authored by whoever owns the page; config is often set by a shared wrapper or component library that the page author cannot edit. So <connectly-webchat side="left"> wins over config={{ launcher: { side: 'right' } }}.

One consequence worth knowing: title is a global HTML attribute, so anything that sets it on the element — a CMS, a framework, a tooltip helper — overrides config.panel.title. See the title row under "Attributes" below. If you set the panel heading through config, make sure nothing is also writing a title attribute.

init() is the one place the shorthands and config are reconciled for you: the title/width/height options land as attributes, so when a single init() call passes both a shorthand and a config for the same field, the shorthand yields to config and logs a warning rather than beating it from the top tier.

<ConnectlyWebchat
  clientKey="<your-client-key>"
  config={{
    theme: { accent: '#6c2bd9', colorScheme: 'auto' },
    launcher: { side: 'left', label: 'Chat with us' },
    panel: { width: 380, height: 560 },
  }}
/>

Outside React, assigning config alone repaints nothing — it is a plain field, not an attribute, so call reload() after:

const el = document.querySelector('connectly-webchat');
el.config = { theme: { accent: '#c026d3' } };
el.reload();

Each distinct config value costs a network round-trip. Values are compared structurally, so an equal object is free — but bind config to a colour picker and you send one request per intermediate value. Memoise it, apply on commit, or for a live preview drive the --cwc-* custom properties instead (see "Theming with CSS" below).

config fields

config is a partial WidgetUi — the same shape the dashboard saves.

  • launcherside, offsetX, offsetY, size, shape (circle | pill), label, ariaLabel, icon ({ kind: 'default' | 'url' | 'none', url })
  • panelwidth, height, radius
  • themeaccent, accentText, surface, surfaceAlt, text, textMuted, border, bubbleVisitor, bubbleVisitorText, bubbleAgent, bubbleAgentText, bubbleRadius, fontFamily, colorScheme (light | dark | auto)
  • zIndex
  • collisionbottomOffsetPx, maxBottomOffsetPx
  • mobilebreakpointPx, launcher (offsetX, offsetY, size), panel (fullscreen, width, height, radius0 means "inherit the desktop value")

Visible in autocomplete but not yet acted on, so setting them does nothing: behavior.autoOpen*, navigation.linkTarget, paths.*, panel.anchor, launcher.hideOnMobile, collision.bottomOffsetSelectors, collision.hideBehindModalDialog, position, bundleVersion.

Only the config stored for your business is range-checked. Values passed through config or an attribute are merged in as given, so panel.width: 5000 is a 5000px panel. Use the dashboard's ranges as your bounds. Note this cuts both ways with the precedence order above: an out-of-range attribute is not clamped and cannot be corrected by config.

And fontFamily names a face, it cannot load one: @font-face is ignored inside a shadow root, so declare the face in your own stylesheet and name it here.

Attributes

Every attribute is optional except client-key.

attribute meaning
client-key Required.
title The panel heading — not a tooltip, despite the attribute name. Load-bearing config: as the top precedence tier it overrides config.panel.title, so do not set it for tooltip purposes.
width, height Panel size in bare pixels: width="380". "380px" parses to NaN and is ignored. Not range-checked — see "Only the config stored for your business is range-checked" above. On desktop height is capped at the viewport in CSS (min(…, 100dvh)); width is not, and the mobile panel height is not either.
side left or right.
offset-x, offset-y Launcher offset from its corner, in pixels.
z-index, bottom-offset-px Stacking, and a flat lift above a cookie bar or a sticky cart.
open Panel open. Value-aware: open and open="" are open, open="false" and an absent attribute are closed. In JSX just write open={isOpen}.
log-level silent | error | warn | info | debug. Applies immediately, including after mount.

React props

prop reactive notes
clientKey no Changing it remounts the widget: a different key is a different business, so session, room and transcript all reset.
config yes Presentation overrides. An inline object literal is fine — see the round-trip warning above.
open yes Omit it and the widget owns its own state; pass it and you own it, and a value you never update pins the panel there. Mounting with open={true} comes up showing the panel, with no closed-then-open flash.
onOpenChange (open: boolean) => void, fired for every open and close whoever caused it. On its own, without open, it is the read-only version: the widget stays in charge and you just observe. An inline arrow never re-mounts the widget.

log-level is not a React prop: set it with setAttribute, or write it directly in JSX.

Theming with CSS

The --cwc-* custom properties are the supported theming API. Every resolved value is written to a custom property on the host element, with no !important anywhere in that path, so yours wins:

connectly-webchat {
  --cwc-accent: #6c2bd9;
  --cwc-launcher-size: 56px;
  --cwc-panel-radius: 20px;
}

--cwc-* is desktop and --cwc-m-* is mobile, switched by a media query. Lengths take units here (56px), unlike config, which takes plain numbers. This is also the layer for anything that changes rapidly — it costs no network round-trip at all.

Reaching inside the shadow root is not supported. Class names and structure change without notice; a stylesheet that depends on them will break on an ordinary widget update.

API

Which API you get depends on which integration you chose.

  • The script tag publishes a page global, window.ConnectlyWebchat.
  • The npm package publishes none — the element itself is the API. Reach it from outside your component tree with document.querySelector('connectly-webchat').

window.ConnectlyWebchat — script tag only

The global is published when the widget bundle executes, so it exists in any tag that runs after the bundle's — a defer or type="module" tag — and not in a plain inline script. A plain inline script runs during parsing, and the bundle carries defer, so the inline one runs first no matter where on the page you put it. Where you cannot rely on tag order, register a connectly-webchat:ready listener on document (the event bubbles, and fires when the launcher renders — it is not replayed for a listener added later) and use the global from inside that, or place <connectly-webchat> declaratively and skip the global entirely.

window.ConnectlyWebchat: {
  init(options): Element | null;
  destroy(): void;
  open(): void;
  close(): void;
  toggle(): void;
  isOpen(): boolean;
  isMounted(): boolean;
  element(): Element | null;
  version: string;
}

init(options) accepts:

option meaning
clientKey Required.
title The panel heading.
width, height Panel size. A bare number (380) or a px string ('380px') both work; any other unit is dropped with a console warning.
logLevel silent | error | warn | info | debug.
config Presentation overrides — the same object the npm package's config prop takes.
open Open the panel as soon as it is ready, instead of waiting for a click.
onMountedChange Called with true/false as the widget is added to / removed from the page.
onOpenChange Called with true/false as the panel opens/closes. This is the one you want for a toggle button's label.

Calling init() twice does not create a second widget. It returns the existing element and applies any new options to it — except clientKey, which cannot be changed on a live widget. Remove the old integration first if you need a different key.

isMounted() asks whether the widget is on the page at all (true from shortly after init() until destroy()); isOpen() asks whether the panel is showing. isOpen() is almost always the one you want, paired with onOpenChange.

<connectly-webchat> — both integrations

The element is registered by the script tag and by the package's import alike, and it is the whole API on the npm path. See Configure for the attribute list.

const el = document.querySelector('connectly-webchat');
el.openPanel();
el.closePanel();
el.toggle();
el.open;   // boolean, reflected to the attribute
el.config = { theme: { accent: '#c026d3' } };
el.reload(); // `config` is a plain field; assigning it repaints nothing on its own

In React, prefer the declarative open / onOpenChange props over reaching for the element — the wrapper's onOpenChange is exactly the element's :open and :close events on its own node.

Opening and closing the panel

The names differ by path — open() / close() exist only on the script tag's page global, and the element's methods are openPanel() / closePanel() — but they are equivalent: the global delegates to the element.

// React, declaratively:
<ConnectlyWebchat clientKey="<your-client-key>" open={isOpen} onOpenChange={setIsOpen} />

Events

Events are dispatched on the element and are bubbles / composed, so a page-level listener works without holding a reference to the element:

document.addEventListener('connectly-webchat:open', (e) => {
  console.log(e.detail);
});
event when
connectly-webchat:ready The launcher has rendered. Carries the widget version and configSource, which is default when the config had not arrived yet.
connectly-webchat:open The panel opened, whoever caused it.
connectly-webchat:close The panel closed, whoever caused it.
connectly-webchat:error Something failed hard enough to stop the conversation.
connectly-webchat:config-warning A config value was rejected or ignored.

Payloads are on event.detail — see WebchatElementEventDetail in the package's types.

Version

WEBCHAT_ELEMENT_VERSION (exported from the package, carried on the ready event, and available as window.ConnectlyWebchat.version on the script-tag path) identifies the widget code. It is the first thing Connectly support will ask for. The npm package carries its own version, on its own schedule; the two are not expected to match.

Troubleshooting

The widget fails quietly more often than it fails loudly, which is what makes these worth naming.

The launcher appears, but chat does not work — or it looks unstyled

Check the client key first. An unrecognized or revoked key makes the widget fall back to a default appearance and fail to start a conversation, without stopping the launcher from rendering. So a launcher on the page proves the script loaded; it proves nothing about the key.

Turn logging up (below) and look in the console for a warning naming the client key, or a "widget config unavailable" / "unknown or inactive" message.

A key I just created looks broken for the first minute

Expected, not a bug. A newly issued key can take up to 60 seconds to become active everywhere, and a revoked key can likewise take up to 60 seconds to actually stop working. Wait a minute and reload before concluding the integration is wrong — and do not treat a revocation as an incident response tool on its own.

npm cannot install the git URL

If HTTPS asks for credentials, the two variants that usually help:

# A developer machine set up with an SSH key rather than a token:
npm i git+ssh://git@github.com/connectlyai/webchat.git#v1.1.0

# CI, with a token in the environment and no interactive prompt available:
git config --global url."https://x-access-token:$GITHUB_TOKEN@github.com/".insteadOf "https://github.com/"

Repository not found, or a credential prompt that will not take your password, almost always means the token lacks read access to the repository — not that the tag is wrong.

Two launchers, or a transcript that splits in half

You have two integrations on one page — almost always the paste-in <script> and the npm package, one of them added by a tag manager or a theme rather than by you. Nothing errors, because multiple widgets on one page is a supported case. Search the rendered page for both webchat2026.min.js and any bundled import of @connectly/webchat, and remove one.

ReferenceError: HTMLElement is not defined at build or boot

The package was imported on the server. The import itself throws, before any component renders, because the module defines a custom element extending HTMLElement. This includes @connectly/webchat/react, and it includes a Next.js client component, which is still prerendered during next build. The fix is a browser-only import path — see "Server rendering" under Install.

Vue warns on every render, or Angular throws NG0304

Both mean the framework does not know <connectly-webchat> is a custom element.

  • Vue — add isCustomElement to the compiler options.
  • Angular — add CUSTOM_ELEMENTS_SCHEMA to the component's schemas.

Vue's failure is the nastier of the two: it warns rather than throwing, and emits no element at all, so the page comes up looking like the script simply did not load.

The element moved itself into <body> and my framework broke

An ancestor with a transform, filter or contain becomes the containing block for position: fixed, so the element relocates itself to <body> — and a framework that still thinks it owns that node will throw on the next update. Append the element to <body> yourself, or use the React wrapper, which expects it.

More logging

Raise the console verbosity of a production build:

<connectly-webchat client-key="<your-client-key>" log-level="debug"></connectly-webchat>
window.ConnectlyWebchat.init({ clientKey: '<your-client-key>', logLevel: 'debug' });

That separates "wrong key" from "wrong integration" from "network": you see whether the config loaded, whether a session minted, and whether the socket connected.

Support

Escalate through your usual Connectly channel with the widget version (window.ConnectlyWebchat.version, or the exported WEBCHAT_ELEMENT_VERSION) and your client key, plus a console log captured at log-level="debug".