# Widget Installation

> Add the Reaktly chat widget to any website — script tag, deferred loader, framework notes, and verification.

URL: https://docs.reaktly.com/docs/getting-started/widget-installation

## Prerequisites

- A Reaktly operator with a widget configuration
- Your **operator key** (`op_…`) — reveal it in the dashboard under the widget's **Installation** tab (**Reveal key**)
- The domains that may embed the widget, listed under **Allowed domains** (see [Restrict embedding](#restrict-embedding))

## 1. Embed the widget

Paste this snippet into your site, just before the closing `</body>` tag:

```html
<script type="module">
  import "https://storage.googleapis.com/reaktly-solis-cdn/latest/solis-widget.js";

  window.solis("init", {
    operatorKey: "op_live_…",
  });
</script>
```

The bundle is a side-effecting ES module: importing it registers the global `window.solis(...)` command. `init` fetches your widget configuration and renders the launcher.

> **Note:**
> The dashboard shows the same snippet with your operator key filled in — copy it from the widget's **Installation** tab.

### Optional init options

| Option | Type | Description |
|---|---|---|
| `operatorKey` | string | **Required.** Your operator's public key (`op_…`) |
| `locale` | string | Force the widget language, e.g. `"de"`. Omit to let the widget use the visitor's browser language |
| `contact.userId` | string | Your identifier for the visitor — lets the AI and your operators address them consistently |
| `contact.email` | string | Visitor email |
| `contact.name` | string | Visitor name |
| `showLauncher` | boolean | Set `false` when your own button should open the chat ([client-owned trigger](#client-owned-trigger)) |
| `panelId` | string | `id` for the widget element, so your own trigger can point at it with `aria-controls` |

## 2. Verify the installation

1. Load the page in a browser and wait for the launcher to appear in the bottom corner.
2. Open the chat and send a message — it should be answered from your knowledge base.
3. Watch the browser console: the widget reports a missing or rejected operator key there instead of failing silently.

## Deferred loading (recommended for content sites)

The bundle is ~150 kB gzipped. The **loader** is the small alternative: it installs `window.solis` as a queue (about 1 kB) and fetches the bundle only when the first visitor actually interacts with the chat.

```html
<script src="https://storage.googleapis.com/reaktly-solis-cdn/latest/solis-loader.umd.js"></script>
<script>
  window.solis("init", { operatorKey: "op_live_…" });
</script>

<button type="button" data-solis-toggle aria-expanded="false">Questions?</button>
```

- Any element with a `data-solis-*` attribute becomes a trigger — no inline JavaScript, which makes this work in CMS-built headers.
- The first click waits for the download. To warm the bundle up in the background instead, add `data-idle="3000"` (milliseconds) or `data-preload="eager"`.
- The loader state is observable on the document element: `<html data-solis-loader>` is `idle`, `loading` or `loaded`.

## Pin a version in production

`latest/` always serves the newest release — convenient while evaluating, but a moving target. For production, pin the bundle and verify its integrity:

```html
<link
  rel="stylesheet"
  href="https://storage.googleapis.com/reaktly-solis-cdn/v1.26.0/solis-widget.css"
  integrity="sha384-…"
  crossorigin="anonymous"
/>
<script
  type="module"
  src="https://storage.googleapis.com/reaktly-solis-cdn/v1.26.0/solis-widget.js"
  integrity="sha384-…"
  crossorigin="anonymous"
></script>
<script>
  window.solis("init", { operatorKey: "op_live_…" });
</script>
```

The hashes come from `…/v1.26.0/integrity.json` (one `sha384` per shipped file). `crossorigin="anonymous"` is required — without it the browser does not check the hash. A pinned version is never overwritten, so rolling back means pointing at the previous version.

## Framework notes

### Next.js / React

Import the bundle in a client component and initialise it once:

```tsx
"use client";

import { useEffect } from "react";

export function ReaktlyWidget({ operatorKey }: { operatorKey: string }) {
  useEffect(() => {
    let active = true;
    import(
      "https://storage.googleapis.com/reaktly-solis-cdn/latest/solis-widget.js"
    ).then(() => {
      if (!active) return;
      window.solis("init", { operatorKey });
    });
    return () => {
      active = false;
      window.solis("destroy");
    };
  }, [operatorKey]);

  return null;
}
```

A React component package is on the roadmap; until it ships, the effect above is the supported integration.

### WordPress

Add the snippet from step 1 to your theme's `footer.php`, or use a "insert headers and footers" plugin. A dedicated Reaktly plugin is on the roadmap.

### Plain HTML

The snippet from step 1 is all you need — it works on static sites and any server-rendered stack.

## Control the widget from your page

Every command is a `window.solis(...)` call:

| Command | Effect |
|---|---|
| `window.solis('open')` / `('close')` / `('toggle')` | Open, close, or toggle the chat window |
| `window.solis('show')` / `('hide')` | Show or hide the launcher (e.g. on checkout pages) |
| `window.solis('update', options)` | Update the configuration without a reload |
| `window.solis('destroy')` | Remove the widget and its listeners |

### Events

Subscribe with `window.solis('on', event, handler)` and unsubscribe with `off`:

| Event | Fires when |
|---|---|
| `ready` | The widget finished initialising |
| `open` / `close` | The chat window opened or closed |
| `message:sent` / `message:received` | A message left or arrived |
| `error` | The widget hit an error |

Every event is also dispatched as a DOM event (`solis:open`, `solis:close`, …) on `window`, `document`, and the widget element — so plain HTML pages can react without the JavaScript API.

### Client-owned trigger

When your own button should open the chat, switch off the launcher and drive the widget yourself:

```html
<script type="module">
  import "https://storage.googleapis.com/reaktly-solis-cdn/latest/solis-widget.js";

  window.solis("init", {
    operatorKey: "op_live_…",
    showLauncher: false,
    panelId: "support-chat",
  });
</script>

<button type="button" onclick="window.solis('toggle')">Chat with us</button>
```

Focus is handled for you: the widget remembers what opened it and returns focus there on close. Set `panelId` if you want `aria-controls` on your trigger, and reflect `aria-expanded` from the `solis:open` / `solis:close` events.

## Restrict embedding

**Allowed domains** in the widget settings restrict which origins may use your widget (OriginGuard). One pattern per line:

| Pattern | Matches |
|---|---|
| `example.com` | Exact domain |
| `*.example.com` | All subdomains |
| `https://app.example.com` | Specific protocol and domain |

Leaving the list empty allows every domain. Once you list domains, only matching origins are accepted.

## Browser support

| Engine | Minimum |
|---|---|
| Chrome / Edge | 111 |
| Safari | 16.2 |
| Firefox | 113 |

The floor is set by `color-mix()` in the widget's design tokens. There is no transpiled build — below the floor the widget renders nothing rather than breaking the layout.

## Troubleshooting

| Symptom | Check |
|---|---|
| Nothing renders | Console for an operator-key error; the key was revealed for the correct operator |
| Widget renders, but requests are rejected | The page's origin is missing from **Allowed domains** |
| Launcher missing, chat opens via your button | Expected when `showLauncher: false` |
| Chat opens but stays empty | The operator's widget configuration failed to load — check the network tab for the config request |

## Next steps

- [Your First Conversation](/docs/getting-started/first-conversation) — test the assistant against your content
- [Widget Customization](/docs/platform/widget-customization) — colors, copy, and behaviour
- [Knowledge Base Basics](/docs/getting-started/knowledge-base-basics) — what the AI answers from