# Installation

> Every install path in depth: script tag, async loader, npm, web component, CDN URLs and version pinning.

Deep-dive for every install path. See [Quickstart](/sdk/quickstart)
for the 5-minute version.

---

## Choose your path

| Path                                                              | Best for                                            | Delivery   |
| ----------------------------------------------------------------- | --------------------------------------------------- | ---------- |
| [A. `<script data-*>` auto-boot](#path-a--script-data--auto-boot) | Static HTML, WordPress, no build step               | CDN        |
| [B. Async loader snippet](#path-b--async-loader-snippet)          | Tag managers, analytics pipelines                   | CDN        |
| [C. NPM package](#path-c--npm-package)                            | React, Next.js, Vue, Svelte, SolidJS, any bundler   | NPM        |
| [React kit](/sdk/frameworks#react)                                | React, Next.js, Remix: `<HelpAI />` + `useHelpAI()` | NPM        |
| [D. Web Component](#path-d--web-component)                        | Declarative tags in any framework                   | CDN or NPM |

> ### CDN URL
>
> The bundle is served from a **versioned** path — the bare root 404s:
>
> ```
> https://cdn.help.ai/sdk/js/elements/<version>/<file>
> ```
>
> - `…/elements/<version>/elements.js` — **pinned** to an exact release (immutable, recommended for production).
> - `…/elements/latest/elements.js` — always the newest release (a 302 to its pinned path, cached up to 4 hours).
>
> Files: `elements.js` (auto-boot + queue + tag), `elements-web-component.esm.js`
> (standalone web-component ESM). The examples below use `latest`; **pin a version
> in production** so a release never changes behaviour under you.

All four paths:

- Load the same JS bundle.
- Accept the same [configuration options](/sdk/configuration).
- Emit the same [events](/sdk/events-and-hooks).
- Respect the same [isolation guarantees](/sdk/security-privacy#shadow-dom--iframe).

You can combine them — e.g., a Next.js page can use path C, and a
marketing landing can use path A, both pointing at the same deployment.

---

## Path A — `<script data-*>` auto-boot

Fastest path. Zero custom JavaScript.

```html
<!-- In your HTML <head> -->
<script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js" data-public-key="agent_pk_abc123..." data-ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" defer></script>
```

### Attributes

The zero-JavaScript path intentionally exposes a small, flat subset of
`ChatWidgetOptions`. These are the attributes parsed by the current bundle:

| Attribute                                      | Maps to                           | Example                         |
| ---------------------------------------------- | --------------------------------- | ------------------------------- |
| `data-public-key`                              | `publicKey`                       | `agent_pk_...`                  |
| `data-ai-agent-deployment-id`                  | `aiAgentDeploymentId`             | deployment UUID                 |
| `data-base-url`                                | `baseUrl`                         | `https://help.ai`               |
| `data-agent-api-base-url`                      | `agentApiBaseUrl`                 | `http://localhost:3069`         |
| `data-api-base-url`                            | `dataApiBaseUrl`                  | `http://localhost:3071`         |
| `data-theme`                                   | `theme`                           | `auto` / `light` / `dark`       |
| `data-mode` / `data-position`                  | `presentation`                    | `floating` / `bottom-right`     |
| `data-iframe`                                  | `presentation.iframe`             | `true`                          |
| `data-width` / `data-height`                   | `presentation.size`               | `420px` / `640px`               |
| `data-response-mode`                           | `behavior.responseMode`           | `streaming` / `buffered`        |
| `data-start-minimized`                         | `behavior.startMinimized`         | `false`                         |
| `data-show-reasoning`                          | `behavior.showReasoning`          | `true`                          |
| `data-show-tool-calls`                         | `behavior.showToolCalls`          | `true`                          |
| `data-show-sources`                            | `behavior.showSources`            | `true`                          |
| `data-disable-message-feedback`                | `behavior.disableMessageFeedback` | `true`                          |
| `data-auto-send-suggestions`                   | `behavior.autoSendSuggestions`    | `true`                          |
| `data-pop-out-url`                             | `behavior.popOutUrl`              | `https://chat.example.com`      |
| `data-locale`                                  | `i18n.locale`                     | `fr-FR`                         |
| `data-launcher-variant` / `-size` / `-icon`    | `launcher` fields                 | `pill` / `lg` / `spark`         |
| `data-launcher-label` / `-hidden`              | `launcher` fields                 | `Ask AI` / `true`               |
| `data-callout-text` / `-delay`                 | `launcher.callout`                | `Hi! 👋` / `2000`               |
| `data-actions`                                 | `header.actions` (CSV)            | `theme,history,close`           |
| `data-features-disable-file-upload`            | `features.disableFileUpload`      | `true`                          |
| `data-features-voice`                          | `features.voice`                  | `local` / `server` / `disabled` |
| `data-endpoint-upload` / `-transcribe`         | `endpoints`                       | `/pai/upload`                   |
| `data-user-id` / `-email` / `-name`            | `userContext` scalar fields       | `user_42`                       |
| `data-user-context`                            | `userContext` extras (JSON)       | `'{"plan":"pro"}'`              |
| `data-page-area` / `-url` / `-path` / `-title` | `pageContext` scalar fields       | `support`                       |
| `data-page-context`                            | `pageContext` extras (JSON)       | `'{"experiment":"b"}'`          |

Unknown attributes are ignored. Full option semantics:
[Configuration](/sdk/configuration).

> Explicit scalar attributes (`data-user-id`, `data-page-area`, …) win
> over keys in the `data-user-context` / `data-page-context` JSON. For
> SPAs where the visitor logs in after load or navigates between pages,
> set context dynamically with
> [`instance.setUserContext(...)` / `setPageContext(...)`](/sdk/configuration#setting-context-after-login--on-navigation).
> The `data-user-*` / `data-page-*` attributes are reactive — updating
> one re-handshakes with the same anonymous visitor/conversation. This
> context is informational for the AI, never an auth credential.

### Load without mounting

The script tag mounts the widget only when it carries `data-ai-agent-deployment-id`. Leave that attribute out to
load the bundle **without** mounting, then call `init()` from JavaScript when you're ready:

```html
<script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js"></script>
<script>
  // Mount later, conditionally
  document.addEventListener('DOMContentLoaded', () => {
    if (shouldShowChat()) {
      HelpAI.init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000' });
    }
  });
</script>
```

### Multi-widget

For two widgets on one page, load the bundle once and call `mount()` with
distinct `widgetId` values:

```js
HelpAI.mount({ widgetId: 'support', publicKey: SUPPORT_KEY, aiAgentDeploymentId: SUPPORT_DEPLOYMENT_ID });
HelpAI.mount({ widgetId: 'sales', publicKey: SALES_KEY, aiAgentDeploymentId: SALES_DEPLOYMENT_ID });
```

---

## Path B — async loader snippet

Tag-manager-style snippet. Defers the bundle download without blocking
first paint, while letting your page queue commands immediately.

```html
<script>
  (function (w, d) {
    w.HelpAI =
      w.HelpAI ||
      function () {
        (w.HelpAI.q = w.HelpAI.q || []).push(arguments);
      };
    w.HelpAIHooks = w.HelpAIHooks || [];
    var s = d.createElement('script');
    s.async = 1;
    s.src = 'https://cdn.help.ai/sdk/js/elements/latest/elements.js';
    d.head.appendChild(s);
  })(window, document);

  // Queue commands — they run in order once the bundle arrives.
  HelpAI('init', { publicKey: 'agent_pk_abc123...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000' });
</script>
```

### Queueable commands

| Call                                      | What it does                                                     |
| ----------------------------------------- | ---------------------------------------------------------------- |
| `HelpAI('init', opts)`                    | Mount once; update the same widget id                            |
| `HelpAI('mount', opts)`                   | Mount fresh; replace the same widget id                          |
| `HelpAI('destroy')`                       | Unmount all live widgets                                         |
| `HelpAI('open')`                          | Open the default widget's panel                                  |
| `HelpAI('close')`                         | Close the default widget's panel                                 |
| `HelpAI('expand')`                        | Toggle the default widget's expanded tier                        |
| `HelpAI('ask', text, { send })`           | Open the chat with `text` in the composer; `send: true` sends it |
| `HelpAI('on', eventName, fn)`             | Subscribe to a default-widget event                              |
| `HelpAI('update', { /* patch */ })`       | Update the default widget's options                              |
| `HelpAI('hook', widgetId, eventName, fn)` | Register a global lifecycle hook                                 |

### Pre-boot hooks

`window.HelpAIHooks` is a pre-boot queue for event listeners. Push
arrays or objects to it before the bundle arrives:

```html
<script>
  window.HelpAIHooks = window.HelpAIHooks || [];

  // Fire for every event on every widget
  window.HelpAIHooks.push([
    '*',
    '*',
    (inst, event, data) => {
      console.log('chat event:', event, 'from', inst.widgetId, data);
    },
  ]);

  // Or target one widget + one event
  window.HelpAIHooks.push([
    'support',
    'message',
    (inst, _event, msg) => {
      analytics.track('chat_message', msg);
    },
  ]);
</script>
```

Full details: [Events & hooks](/sdk/events-and-hooks#global-lifecycle-hooks).

### After boot

Once the bundle loads, `window.HelpAI` becomes a real API:

```js
window.HelpAI.init(opts)
window.HelpAI.destroy(widgetId?)
window.HelpAI.getInstance(widgetId?)     // returns handle or null
window.HelpAI.getAllInstances()
window.HelpAI.hook(widgetId, event, fn)
window.HelpAI.open() / close() / expand()
window.HelpAI.version                    // semver string
window.HelpAI.brand                      // { id, displayName, cdnUrl, ... }
```

---

## Path C — NPM package

For sites with a bundler. Full framework-specific walkthroughs:
[Framework integrations](/sdk/frameworks).

### Install

```bash
npm install @helpai/elements
# or
yarn add @helpai/elements
# or
pnpm add @helpai/elements
# or
bun add @helpai/elements
```

### Entry points

The package ships two entries:

```ts
// Main entry — everything except the web component.
import { init, mount, destroy, hook } from '@helpai/elements';
// Sub-entry — registers <web-ai-chat> as a custom element.
import '@helpai/elements/web-component';
```

Tree-shaking: the main entry has zero mount-on-import side effects.
Unused exports are dropped by your bundler. Importing only types costs
zero runtime bytes.

### Minimal React usage

```tsx
import { init, type ChatWidgetInstance } from '@helpai/elements';
import { useEffect, useRef } from 'react';

export function Chat() {
  const instance = useRef<ChatWidgetInstance | null>(null);

  useEffect(() => {
    instance.current = init({
      publicKey: import.meta.env.VITE_HELPAI_KEY,
      aiAgentDeploymentId: import.meta.env.VITE_HELPAI_DEPLOYMENT_ID,
    });
    return () => instance.current?.destroy();
  }, []);

  return null; // the widget renders its own DOM outside your tree
}
```

### Typed options

Autocomplete for every option:

```ts
import type { ChatWidgetOptions } from '@helpai/elements';

const opts: ChatWidgetOptions = {
  publicKey: 'agent_pk_...',
  aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
  presentation: { mode: 'floating' },
  theme: { accent: '#ff3e00' },
  launcher: { variant: 'pill', label: 'Ask AI' },
  // ...
};
```

### SSR

The widget is client-only. Never call `init()` on the server. In
Next.js app router, wrap your component with `'use client'` (shown
above). In Remix / SvelteKit, use the `onMount` / `useEffect`
equivalent to defer init until the browser.

---

## Path D — Web Component

Declarative tag. Works in any framework or plain HTML.

### Load + use

```html
<!-- Load the bundle once (anywhere in the page) -->
<script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js" defer></script>

<!-- Then use the element anywhere, any number of times -->
<web-ai-chat public-key="agent_pk_..." ai-agent-deployment-id="..." mode="inline" theme="auto" style="display: block; height: 480px"></web-ai-chat>
```

### Attributes

Same parser fields as `<script data-*>`, but without HTML's outer
`data-` dataset prefix. Values are strings (booleans are `"true"` /
`"false"`). `data-api-base-url` keeps its name because `data` refers to
the data API.

Reactive attributes re-resolve the live widget. `iframe` and
`start-minimized` are mount-time-only.

```js
document.querySelector('web-ai-chat').setAttribute('theme', 'dark');
// The widget flips to dark mode.
```

The element also has the panel methods: `open()`, `close()`, `expand()`, `openForm(id)` and `ask(text, options)`,
which opens the chat with `text` in the composer for the visitor to send (`{ send: true }` sends it):

```js
document.querySelector('web-ai-chat').ask('How do I book a session?');
document.querySelector('web-ai-chat').ask('How do I book a session?', { send: true });
```

### Default mode

A bare `<web-ai-chat>` tag initially resolves to `mode="floating"`;
the handshake's `config.presentation.mode` supplies the deployment mode
when the host did not set one.

### Container sizing (`inline`, `page`, `standalone`)

These modes have the host element declare `width: 100%; height: 100%`
inside its shadow root. Width fills the parent automatically; **height
only resolves if the parent has a defined height**. An undefined parent
height collapses the widget to `0px` tall and it appears empty.

```html
<!-- ✓ Viewport-filling page -->
<html style="height: 100%">
  <body style="height: 100%; margin: 0">
    <web-ai-chat mode="page" public-key="…" ai-agent-deployment-id="…"></web-ai-chat>
  </body>
</html>

<!-- ✓ Sized container (any of px / vh / 100% chain) -->
<div style="height: 600px">
  <web-ai-chat mode="page" public-key="…" ai-agent-deployment-id="…"></web-ai-chat>
</div>

<!-- ✓ Flex / grid child with explicit sizing -->
<body style="display: flex; flex-direction: column; height: 100vh; margin: 0">
  <header>…</header>
  <web-ai-chat mode="page" style="flex: 1 1 0; min-height: 0" …></web-ai-chat>
</body>

<!-- ✗ Body has no defined height → host's 100% resolves to 0 -->
<body>
  <web-ai-chat mode="page" …></web-ai-chat>
</body>
```

`floating` mode is unaffected — the host is `position: fixed` and
the panel's size is set by `panelSize` (`normal` / `expanded` /
`fullscreen`), independent of the parent. `modal` and `drawer` are
also self-sized: modal anchors to the viewport center; drawer docks
to the side. See `../features/modes.md` §
4.4 for the full sizing spec.

### Inside a framework

Most frameworks treat custom elements like any other HTML element:

**React 19+** — native support:

```tsx
<web-ai-chat public-key='agent_pk_...' ai-agent-deployment-id='01900000-0000-7000-8000-000000000000' mode='inline' />
```

**React < 19** — the element works but TypeScript doesn't know the
custom type. Declare it:

```ts
declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      'web-ai-chat': {
        'public-key'?: string;
        mode?: 'floating' | 'inline' | 'standalone' | 'page' | 'modal' | 'drawer';
        theme?: 'auto' | 'light' | 'dark';
        // ... (full list in configuration.md)
      };
    }
  }
}
```

**Vue 3** — works out of the box.

**Svelte** — works with `<svelte:options>`-less components.

**Angular** — add `CUSTOM_ELEMENTS_SCHEMA` to your module.

Framework-by-framework details: [Framework integrations](/sdk/frameworks).

---

## Version pinning

The bundle URL `https://cdn.help.ai/sdk/js/elements/latest/elements.js` always serves the
latest stable version. For reproducibility, pin to a specific
version:

```html
<script src="https://cdn.help.ai/v1/elements.js" defer></script>
<!-- or -->
<script src="https://cdn.help.ai/v1.2.0/elements.js" defer></script>
```

Pinning means you control upgrade timing. The floating URL means you
get new features + bug fixes automatically. Both are supported; most
customers use the floating URL for marketing sites and pinned
versions for mission-critical apps.

---

## Path E — Standalone ESM bundle (no build, no NPM install)

For pages without a build step (plain HTML, MDX, sandbox demos, code
playgrounds, AI-generated UIs), load the pre-bundled ESM file shipped
with every release. `elements.esm.js` is fully self-contained — every
dependency (preact, signals, streaming-markdown) is inlined. No bare
specifiers, no import maps, no `+esm` magic. Drop into any page with
`<script type="module">`.

### Programmatic mount

```html
<script type="module">
  // Pin an exact version for production (see Caching & versioning below).
  import { mount } from 'https://cdn.jsdelivr.net/npm/@helpai/elements@<version>/elements.esm.js';

  mount({
    publicKey: 'agent_pk_…',
    aiAgentDeploymentId: '…',
    presentation: { mode: 'inline' },
    target: document.querySelector('#chat'),
  });
</script>
```

### Web component (declarative tag)

The web-component bundle self-registers `<web-ai-chat>` on import:

```html
<script type="module">
  import 'https://cdn.jsdelivr.net/npm/@helpai/elements@<version>/elements-web-component.esm.js';
</script>

<web-ai-chat public-key="agent_pk_…" ai-agent-deployment-id="…" mode="inline"></web-ai-chat>
```

### File catalog (every `dist/<brand>/` ships these)

| File                              | Purpose                                                | Use with                                |
| --------------------------------- | ------------------------------------------------------ | --------------------------------------- |
| `elements.js`                     | IIFE bundle, defines `window.HelpAI`                   | Plain `<script>` (no `type="module"`)   |
| `elements.esm.js`                 | **Standalone ESM, fully bundled**                      | `<script type="module">` import         |
| `elements-web-component.esm.js`   | **Standalone ESM, registers `<web-ai-chat>`**          | `<script type="module">` import (no JS) |
| `index.mjs` / `web-component.mjs` | tsdown ESM with bare imports (`import "preact"` etc.)  | Bundlers (Vite, webpack, esbuild)       |
| `react.mjs` / `react.d.ts`        | React kit: `<HelpAI />`, `useHelpAI()`, widget bundled | React apps, `@helpai/elements/react`    |
| `style.css`                       | Standalone stylesheet — host-element tokens + layout   | `<link rel="stylesheet">`               |
| `loader.js`, `loader.min.js`      | GA-style queueing loader stub                          | Tag managers / analytics pipelines      |
| `index.d.ts`                      | TypeScript declarations                                | NPM consumers                           |

### CDN URL forms

| URL                                                                               | Resolves to                                        |
| --------------------------------------------------------------------------------- | -------------------------------------------------- |
| `https://cdn.help.ai/sdk/js/elements/latest/elements.js`                          | IIFE on the brand CDN                              |
| `https://cdn.help.ai/sdk/js/elements/latest/elements.esm.js`                      | Standalone ESM on the brand CDN                    |
| `https://cdn.jsdelivr.net/npm/@helpai/elements@<v>/elements.esm.js`               | Standalone ESM on jsDelivr (immutable per version) |
| `https://cdn.jsdelivr.net/npm/@helpai/elements@<v>/elements-web-component.esm.js` | Web-component ESM on jsDelivr                      |

### Version pinning

| Pattern      | Behavior                                          |
| ------------ | ------------------------------------------------- |
| `@1.4.0`     | Exact version. Immutable, max-age cached forever. |
| `@1`, `@1.4` | Semver-range — latest matching `1.x.x` / `1.4.x`. |
| `@latest`    | Always the most recent published version.         |
| `@^1.4.0`    | Caret range, just like in `package.json`.         |

> **Production tip.** Pin a specific version for production traffic so
> a published patch can't surprise you. `@latest` is fine for
> playgrounds and internal tools.

> **Why not `+esm`?** jsDelivr's `/+esm` flag tries to auto-bundle on
> demand and trips on bare-specifier resolution for some packages —
> `Failed to resolve module specifier "preact"` is the common symptom.
> Loading the pre-bundled `elements.esm.js` avoids that entirely:
> built once at release time, immutable forever, every dep inlined.

---

## Caching, versioning & self-hosting

Two delivery models. Pick by how much you control the URL the customer pastes.

### Option 1 — public CDN, pinned version (no infra; the default today)

Serve the published NPM package straight from **jsDelivr**, pinned to an exact
version — swap `<version>` for the release you want (the immutable pinning the live demos use):

```html
<script type="module" async src="https://cdn.jsdelivr.net/npm/@helpai/elements@<version>/elements-web-component.esm.js"></script>
```

- jsDelivr serves a pinned version as `Cache-Control: public, max-age=31536000,
  immutable` — cached for a year, global edge, free.
- **Always pin an exact version** for production. `@latest` / ranges (`@0.56`)
  are mutable (jsDelivr revalidates ~daily) **and** auto-update — a published
  patch can change behavior under you.
- Trade-off: the customer must **bump the version in their snippet to update**,
  and every release re-downloads the whole ~107 KB (one self-contained file).
- Speed it up on pages you control: `<link rel="preconnect"
  href="https://cdn.jsdelivr.net" crossorigin>` (and optionally `rel="modulepreload"`).

### Option 2 — your own CDN behind a stable URL (recommended; simplest for customers)

Point a CDN (`cdn.help.ai`, a Cloudflare/CloudFront origin, …) at the built
`dist/<brand>/` assets and give customers **one URL that never changes**:

```html
<!-- The customer pastes this ONCE and never touches it again. -->
<script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js" data-public-key="agent_pk_..." data-ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" defer></script>
```

You ship updates **behind** that stable URL — the customer's page is frozen. The
caching rule is the whole trick:

| Asset                                 | `Cache-Control`                        | Why                                          |
| ------------------------------------- | -------------------------------------- | -------------------------------------------- |
| The stable entry (`elements.js`)      | `public, max-age=300, must-revalidate` | Short TTL so a deploy propagates in minutes. |
| Content-hashed chunks (`*.<hash>.js`) | `public, max-age=31536000, immutable`  | Hash in the name → safe to cache forever.    |

Plus Brotli/Gzip, HTTP/2+3, and (optionally) `integrity="sha384-…"` SRI hashes.

### Automatic chunking (zero customer changes)

The value of Option 2 is the **stable URL** above — the customer's snippet never
changes and you own caching + region. Chunking is a further, _optional_ step on
top (usually **not** worth it — see the caveat below). With **content-hashed
chunk filenames served from a version-less path**, an unchanged chunk keeps the
**same URL** across releases, so the browser reuses it — only the chunks that
actually changed re-download. To get there:

1. Build ESM with code-splitting on (esbuild/tsdown `splitting: true`, `format:
   "esm"`) and `[hash]` filenames. The thin entry imports the chunks.
2. Add lazy `import()` boundaries at the **optional surfaces** — messenger tabs
   (Home/Help/News), forms/HITL, voice. esbuild emits each as its own chunk and
   hoists shared code (e.g. Preact) into a common chunk automatically. Core chat
   loads first → smaller first paint; each feature caches independently.
3. The customer keeps the single stable `<script>` — all splitting, hashing, and
   cache invalidation happen behind it. No snippet change, ever.

> **Why this needs your own CDN — not jsDelivr.** On jsDelivr the version lives in
> the path (`/@0.56.2/…`). When you ship `0.56.3` **every** chunk URL changes —
> even a byte-identical vendor chunk gets `/@0.56.3/vendor.<hash>.js`, a guaranteed
> cache miss. So splitting on a version-pinned CDN trades one request for many of
> the same total bytes — _worse_. Cross-release chunk caching requires
> **content-addressed URLs where the version is not in the path**, i.e. a CDN you
> control. (Splitting still helps **first-paint** on either, via lazy features.)

> **Is it worth it? (usually no.)** We built and measured this for the `@helpai/elements`
> bundle and reverted it: only ~10 % of an ~107 KB gz bundle is lazy-able (the rest is
> core chat that loads on first paint regardless), and the lazy `import()` needed to
> create a chunk is **inlined** into the single-file builds — so it _adds_ ~1 KB there
> for no benefit. The cost lands on the builds that don't chunk. For a single
> self-contained file that's `async` + immutable-cached, the per-release re-download is
> a non-issue anyway. Reach for chunking only if your optional surfaces
> (tabs/forms/voice) grow large enough that lazy-loading them meaningfully shrinks first
> paint — and **measure with an esbuild metafile first**, don't split blind.

> **Data residency.** Self-hosting (Option 2) also lets you keep asset delivery in
> a chosen region; the runtime still calls the agent API at the configured
> `baseUrl`.

---

## What ships in the bundle

- **Preact 11 + @preact/signals** — reactive rendering primitives,
  ~6 KB combined gzipped.
- **streaming-markdown** — incremental markdown renderer, ~3 KB.
- **Widget code** — chat transcript, composer, messenger-module tabs
  (Home / Help / News), forms + HITL, config, themes — the bulk.
- **i18n** — built-in locale strings (EN + bundled translations).
- **CSS** — inlined into the bundle (no separate request).

Total **~107 KB gzipped** for the IIFE / standalone ESM (see the live
budgets in `.size-limit.json`, enforced in CI).
No external runtime dependencies. No React, no jQuery, no analytics
beacons. The bulk is widget code, **not** vendor — relevant when you
weigh chunking (see [Caching, versioning & self-hosting](#caching-versioning--self-hosting)).

---

## Next

- [Configuration](/sdk/configuration) — every option explained with examples.
- [Theming & customization](/sdk/theming) — matching your brand.
- [Framework integrations](/sdk/frameworks) — framework-specific recipes.
- [Troubleshooting](/sdk/troubleshooting) — "my widget isn't showing up".
