# Customize the website widget

> Attributes, colours, JavaScript methods and events for the uare.ai chat widget, plus React, Next.js, iframe and Content Security Policy setup.

The widget is one script that defines a `<uare-chat>` HTML element. You can configure it two ways:

- **On the script tag**, with `data-` attributes. The script then adds a chat bubble to the page by itself:

  ```html
  <script async src="https://app.uare.ai/embed/v1.js" data-handle="your-handle" data-position="bottom-left"></script>
  ```

- **On a `<uare-chat>` element** you place yourself, which is how you put the chat inside your layout. Load the script once per page:

  ```html
  <script async src="https://app.uare.ai/embed/v1.js"></script>

  <uare-chat handle="your-handle" mode="inline" theme="light" style="height: 640px"></uare-chat>
  ```

Your website must be on your AI's list of allowed websites, either way. The AI's owner adds it, in their Settings on uare.ai: see [Allow your website](/docs/website/quickstart#1-allow-your-website).

## Attributes

| Attribute | Values | Default |
| --- | --- | --- |
| `handle` | Your AI's handle, from your public profile's address. Required. | |
| `mode` | `bubble`: a button in the corner that opens the chat. `inline`: the chat fills the element, which you size. | `bubble` |
| `theme` | `auto` follows the visitor's light or dark setting, or force `light` or `dark`. | `auto` |
| `accent-color` | Any CSS colour, for the bubble, the send button and the visitor's messages. The text on it turns black or white to stay readable. When your AI has an avatar, the avatar fills the bubble, so the colour shows only on the send button and the visitor's messages. | Near-black, or near-white in dark mode |
| `position` | `bottom-right` or `bottom-left`, for the bubble. | `bottom-right` |
| `greeting` | The line shown before the first message. | Your AI's bio |
| `placeholder` | The message box's placeholder. | `Message <name>…` |
| `open` | Present to start with the bubble open. On the element, `chat.open = true` or `false` opens or closes the bubble, and `chat.open()` still works: see [JavaScript](#javascript). | Closed |
| `lang` | The visitor's language, as a tag such as `fr` or `es`. It's set on the chat for screen readers and kept with the conversation. Your AI picks the language of its replies itself, and the widget's own labels stay in English: use `greeting` and `placeholder` for the words visitors read first. | `en` |

On the script tag, add `data-` to each name: `data-accent-color="#7c3aed"`. You can change attributes on the element at any time and it updates.

## Colours and fonts

The widget lives in its own shadow DOM, so your site's CSS doesn't leak into it. To restyle it, set these CSS variables on `uare-chat`:

```css
uare-chat {
  --uare-accent: #7c3aed;     /* bubble (when your AI has no avatar), send button, visitor messages */
  --uare-accent-fg: #ffffff;  /* text on the accent */
  --uare-font: "Inter", system-ui, sans-serif;  /* defaults to your page's font */
  --uare-radius: 20px;        /* corners of the chat panel */
  --uare-bg: #ffffff;         /* panel background */
  --uare-fg: #18181b;         /* text */
  --uare-surface: #f4f4f5;    /* your AI's messages */
  --uare-muted: #6b6b72;      /* secondary text */
  --uare-border: #e4e4e7;     /* lines and outlines */
}
```

Variables you set apply in both light and dark mode. Leave one unset to keep the widget's default for the visitor's theme.

## JavaScript

Methods on the element:

| Method | What it does |
| --- | --- |
| `open()` | Opens the bubble. In inline mode, focuses the message box. |
| `close()` | Closes the bubble. |
| `toggle()` | Opens or closes the bubble. |
| `send(text)` | Sends `text` as the visitor's message. Returns a promise that resolves when the reply has finished, and rejects when the message isn't sent or the reply fails. |
| `reset()` | Starts a new conversation. |

`open` is also a property that takes a boolean, the way frameworks such as React 19 set it: `chat.open = true` opens the bubble and adds the `open` attribute, `chat.open = false` closes it and removes the attribute. Reading `chat.open` still gives the method.

When `send()` doesn't send, or the reply fails, its promise rejects with an `Error` whose `code` says why:

| `code` | Why |
| --- | --- |
| `not_ready` | Your AI hasn't loaded yet, or couldn't load. Call `send()` after `uare:ready`. |
| `turn_in_progress` | A reply is still being written. Send the next message once the previous `send()` has settled. |
| `invalid_request` | The text is empty. |
| `message_limit_reached` | The conversation's free messages are used up. The error's `continue_url` is your AI's page on uare.ai, and `uare:limit` fires too. |
| Any other | The reply failed. `code` and `message` are the ones `uare:error` reports, such as `chat_unavailable`. |

If the visitor stops the reply, or `reset()` starts a new conversation while it's streaming, the promise resolves.

Events, which bubble up to `document`. Each one's `detail` is described here:

| Event | `detail` |
| --- | --- |
| `uare:ready` | `{ ai: { handle, name, avatar_url, bio, profile_url } }` when your AI has loaded: once per load, so again only if you change `handle` or `api-base` |
| `uare:open`, `uare:close` | `{}` |
| `uare:message` | `{ role: "user" \| "assistant", text }` for each finished message |
| `uare:limit` | `{ limit, continue_url }` when the conversation's free messages are used up |
| `uare:error` | `{ code, message }` when the widget can't load or a reply fails |

For example, open the chat from your own button and send a first question. This works with the one-line snippet, which adds `<uare-chat>` to the page only once its script has loaded, and with an element you place yourself:

```html
<button id="ask">Ask about pricing</button>

<script>
  document.getElementById("ask").addEventListener("click", () => {
    // Look the element up when it's needed: with the one-line snippet,
    // it isn't on the page yet while this script runs.
    const chat = document.querySelector("uare-chat");
    chat?.send("How does your pricing work?").catch((error) => {
      console.warn(`Not sent (${error.code}): ${error.message}`);
    });
  });

  // The widget's events bubble up to document, so listen there.
  document.addEventListener("uare:message", (event) => {
    console.log(event.detail.role, event.detail.text);
  });
</script>
```

`send()` opens the bubble if it's closed. Call it after `uare:ready`: before your AI has loaded, it rejects with `not_ready` and sends nothing.

## React and Next.js

React 19 renders custom elements directly. Load the script once with `next/script`, then use the element like any other:

```tsx title="Next.js"
"use client";

import Script from "next/script";
import { useEffect, useRef } from "react";

export function AiChat() {
  const ref = useRef<HTMLElement>(null);

  useEffect(() => {
    const chat = ref.current;
    if (!chat) return;
    const onMessage = (event: Event) => {
      const { role, text } = (event as CustomEvent<{ role: string; text: string }>).detail;
      console.log(role, text);
    };
    chat.addEventListener("uare:message", onMessage);
    return () => chat.removeEventListener("uare:message", onMessage);
  }, []);

  return (
    <>
      <Script src="https://app.uare.ai/embed/v1.js" strategy="afterInteractive" />
      <uare-chat ref={ref} handle="your-handle" mode="inline" style={{ height: 640 }} />
    </>
  );
}
```

```tsx title="Type declaration"
// uare-chat.d.ts: lets TypeScript accept <uare-chat> in JSX, and types the element.
import type { DetailedHTMLProps, HTMLAttributes } from "react";

export interface UareChatElement extends HTMLElement {
  /** Reading it gives the open() method; assigning true or false opens or closes the bubble. */
  get open(): () => void;
  set open(value: boolean);
  close(): void;
  toggle(): void;
  /** Resolves when the reply has finished; rejects with an Error whose `code` says why it wasn't sent. */
  send(text: string): Promise<void>;
  reset(): void;
}

type UareChatProps = DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement> & {
  handle: string;
  mode?: "bubble" | "inline";
  theme?: "auto" | "light" | "dark";
  "accent-color"?: string;
  position?: "bottom-right" | "bottom-left";
  greeting?: string;
  placeholder?: string;
  open?: boolean;
  lang?: string;
};

declare global {
  interface HTMLElementTagNameMap {
    "uare-chat": UareChatElement;
  }
}

declare module "react" {
  namespace JSX {
    interface IntrinsicElements {
      "uare-chat": UareChatProps;
    }
  }
}
```

In JSX, `open` is a boolean: React 19 sets it on the element as a property, which opens or closes the bubble. For the methods, type your ref as `useRef<UareChatElement>(null)`.

For a chat you design yourself, use the [public API with the AI SDK](/docs/api/build-your-own#react-with-the-ai-sdk) instead.

## Embed with an iframe

Some site builders and platforms only accept an iframe. uare.ai's own pages refuse to be framed by your site, so host a small chat page on a website you allowed, and frame that page wherever you need the chat:

```html title="chat.html"
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <style>
      html, body { height: 100%; margin: 0; font-family: system-ui, sans-serif; }
      uare-chat { display: block; height: 100%; }
    </style>
  </head>
  <body>
    <script async src="https://app.uare.ai/embed/v1.js"></script>
    <uare-chat handle="your-handle" mode="inline"></uare-chat>
  </body>
</html>
```

```html title="The iframe"
<iframe
  src="https://example.com/chat.html"
  title="Chat with my AI"
  style="width: 100%; height: 600px; border: 0"
></iframe>
```

The widget uses the page's font, so give `chat.html` the font you want. The website that has to be on your list is the one serving `chat.html`. The page holding the iframe can be on any site, because the chat talks to uare.ai from inside the frame. Browsers keep an iframe's storage separate for each website that frames it, so the conversation follows visitors from page to page on one website, and another website framing the same `chat.html` starts a conversation of its own.

> [!NOTE]
> Don't frame your public profile (`https://app.uare.ai/p/your-handle`). It can load in an iframe on `localhost` while you build, but browsers block it on your live domain.

## Content Security Policy

If your site sends a `Content-Security-Policy` header, allow uare.ai to serve the script and answer the chat:

```http
Content-Security-Policy: script-src 'self' https://app.uare.ai; connect-src 'self' https://app.uare.ai; img-src 'self' https:
```

`img-src` needs to cover your AI's avatar. `https:` allows it wherever it's hosted. The widget's styles need nothing: they're applied from the script, so `style-src` doesn't have to allow inline styles.

## Privacy

- The widget sends no cookies and sets none. The conversation lives in your site's `localStorage`, under keys starting with `uare-chat:`, and leaves when visitors start a new chat or it expires.
- Messages go to uare.ai and your AI answers them in public mode, as on your public profile. uare.ai sees the visitor's IP address, which it uses for rate limits and visitor counts.
- Mention uare.ai in your privacy policy as a service that processes the chat messages visitors send.

## Versions

`/embed/v1.js` stays compatible: updates add features without breaking the attributes, methods and events above. A breaking change would ship as `/embed/v2.js`.
