> ## Documentation Index
> Fetch the complete documentation index at: https://docs.typewise.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Use the Chat SDK with plain HTML and JavaScript

> Load the Chat SDK from a script tag and build a chat with plain DOM code, no build step

Add a Typewise chat to any page with a script tag and a few lines of JavaScript.
No bundler or framework needed.

## Load the SDK

**Script tag**

The script-tag build exposes a global `Typewise` object:

```html theme={null}
<script src="https://platform.typewise.app/client-sdk/typewise.global.js"></script>
```

The Typewise CDN always serves the version that is live on the platform.

**Pinned version**

To lock a version, load it from npm through jsDelivr:

```html theme={null}
<script src="https://cdn.jsdelivr.net/npm/@typewise/chat-sdk@0.1.0/dist/typewise.global.js"></script>
```

**ES module**

Import it directly in a module script:

```html theme={null}
<script type="module">
  import { createClient } from "https://esm.sh/@typewise/chat-sdk";
</script>
```

## Build a chat

This complete page renders the conversation, shows a typing indicator, and sends
messages. Replace `YOUR_CLIENT_ID` and open it in a browser. The highlighted
lines are the only SDK calls.

```html index.html lines highlight={18,25,58,61} theme={null}
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Chat</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <p id="status"></p>
    <ol id="messages" aria-live="polite"></ol>
    <form id="composer">
      <input placeholder="Type a message…" autocomplete="off" />
      <button type="submit">Send</button>
    </form>

    <script src="https://platform.typewise.app/client-sdk/typewise.global.js"></script>
    <script>
      const chat = Typewise.createClient({ clientId: "YOUR_CLIENT_ID" });

      const status = document.getElementById("status");
      const list = document.getElementById("messages");
      const composer = document.getElementById("composer");
      const input = composer.querySelector("input");

      chat.subscribe((state) => {
        status.textContent = state.conversation?.isHandledByHuman
          ? "A team member joined"
          : `Connection: ${state.connection}`;

        const items = state.messages.map((message) => {
          const item = document.createElement("li");
          item.className = `${message.role} ${message.status}`;
          item.textContent = message.text;
          return item;
        });

        if (state.activity.kind !== "idle") {
          const item = document.createElement("li");
          item.className = "activity";
          item.textContent =
            state.activity.kind === "agent_typing"
              ? "Typing…"
              : state.activity.kind === "waiting_for_agent"
                ? "Reviewing…"
                : state.activity.text || "Thinking…";
          items.push(item);
        }

        list.replaceChildren(...items);
        list.scrollTop = list.scrollHeight;
      });

      composer.addEventListener("submit", (event) => {
        event.preventDefault();
        const text = input.value.trim();
        if (!text) return;
        input.value = "";
        chat.sendMessage(text).catch(() => {});
      });

      chat.resume().catch((error) => console.warn("[typewise]", error));
    </script>

  </body>
</html>
```

```css styles.css theme={null}
#messages li.user {
  text-align: right;
}
#messages li.sending {
  opacity: 0.6;
}
#messages li.failed {
  color: crimson;
}
#messages li.activity {
  font-style: italic;
}
```

Each `li` gets the message's `role` and `status` as classes, so you can style
visitor messages, replies, and messages still sending.

> [!TIP]
>
> `textContent` keeps the example safe from injected HTML. If you render
> `message.content` as markdown, sanitize the output first.

## Open the chat from a button

The SDK doesn't render a bubble, so you choose how the chat appears. For
example, keep it in a hidden panel and show it on click:

```javascript theme={null}
document.querySelector("#help-button").addEventListener("click", () => {
  document.querySelector("#chat-panel").hidden = false;
  input.focus();
});
```

## Content Security Policy

If your site sends a CSP header, allow the script host in `script-src` and the
API and real-time domains in `connect-src`. See
[Configure CSP](https://docs.typewise.app/guides/channels/chat-widget/content-security-policy.md#chat-sdk).

## Related

* [Script tag example](https://typewise.github.io/chat-sdk-examples/script-tag/):
  a styled version of this page
* [Render messages and activity](https://docs.typewise.app/guides/channels/chat-sdk/render-messages.md):
  every field you can render
* [Share page context](https://docs.typewise.app/guides/channels/chat-sdk/page-context.md): send what the
  visitor is doing to the AI Agent


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.