# Theming

> Colors, shapes, fonts, launcher styles, theme: "host" and ::part() styling.

Make the widget match your brand — colors, shapes, typography, and
launcher style.

---

## TL;DR

```ts
init({
  publicKey: 'agent_pk_...',
  aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
  theme: {
    accent: '#ff3e00', // your brand primary
    accentText: '#ffffff', // on-accent text
    radius: '12px', // corner rounding
    fontFamily: 'Inter, system-ui, sans-serif',
  },
  launcher: {
    variant: 'pill', // circle / pill / bar / minimal
    label: 'Ask us',
    icon: 'spark', // chat / message / bubbles / spark / wand / bot / help / lifebuoy / headset / search / sparkles / concierge / wave
  },
  composer: {
    sendButton: { shape: 'pill', variant: 'outline' },
  },
});
```

Rest of this page goes deep on each knob.

---

## Theme modes

Four modes:

- **`"auto"`** — follows the visitor's OS preference (`prefers-color-scheme`).
- **`"light"`** — force light mode.
- **`"dark"`** — force dark mode.
- **`"host"`** — follows your page's own light/dark and flips with your site's toggle. The widget reads, in order, a
  `dark` / `light` class on `<html>` or `<body>`, a `data-theme`, `data-color-scheme`, `data-mode` or `data-bs-theme`
  attribute there, then the page's `color-scheme`, then the OS.

```ts
init({ theme: 'auto' }); // default
init({ theme: 'light' });
init({ theme: 'dark' });
init({ theme: 'host' }); // or <web-ai-chat theme="host">, data-theme="host" on the script tag
init({ theme: { mode: 'host', accent: '#635146' } }); // a mode beside token overrides
```

A page that sets only the mode keeps the deployment's token overrides from the dashboard (its brand accent) — so
`<web-ai-chat theme="host">` follows your site without losing the brand colours.

### Runtime switching

End users can flip the theme through the `theme` row in the header's ⋯ menu.
Their choice persists across conversations and syncs
across devices.

If you don't want a runtime picker, remove the action:

```ts
init({
  header: {
    actions: ['textSize', 'history', 'close', 'clear', 'expand', 'fullscreen', 'sound', 'language'],
  },
});
```

---

## Color overrides

Theme properties you can override:

| Option                  | CSS token                   | Purpose                                                                                                                                                                                                                    | Default (help.ai brand)                                 |
| ----------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `theme.accent`          | `--helpai-accent`           | Primary brand color                                                                                                                                                                                                        | `#10B981`                                               |
| `theme.accentText`      | `--helpai-accent-text`      | Text shown on the accent color                                                                                                                                                                                             | `#FFFFFF`                                               |
| `theme.radius`          | `--helpai-radius`           | Corner rounding for buttons, cards, pills                                                                                                                                                                                  | `10px`                                                  |
| `theme.fontFamily`      | `--helpai-font`             | Widget-wide font stack                                                                                                                                                                                                     | `Inter, system-ui, -apple-system, Segoe UI, sans-serif` |
| `theme.contentMaxWidth` | `--helpai-conversation-max` | Max width of the readable content column on a **wide** surface (fullscreen / page / standalone / wide inline) — content centres, bars stay full-bleed. `100%` disables the cap. Modal + drawer fill their card by default. | `768px`                                                 |

```ts
init({
  theme: {
    accent: '#ff3e00',
    accentText: '#fff',
    radius: '16px',
    fontFamily: '"Söhne", -apple-system, sans-serif',
  },
});
```

All of these are optional — any you don't set inherits the brand default.

### Where the accent shows up

Anywhere the widget draws "chrome" in your brand color:

- The launcher FAB background.
- Send / Stop button (when `composer.sendButton.variant: "filled"`).
- Active language/theme menu checkmarks.
- Link colors in rendered markdown.
- Agent badge status dot.
- Focus rings.

### Picking an accent

Pick any brand color — the widget adapts it per theme so it stays readable, so a
low-contrast raw color won't render an unreadable widget:

- `accent`'s hue + chroma are honored; only its **lightness** is nudged into the
  visible band for the active theme (a near-black accent lightens on dark, a
  near-white one darkens on light).
- **On-accent text** (the FAB / send glyph) is auto-derived to black or white, so
  `accentText` is **optional** — set it only to force a specific on-fill color
  (it's used as-is only on browsers without CSS relative-color support).
- **Accent-colored text** (links, active tabs) uses a separate readable shade
  that clears **WCAG AA (4.5:1)** on the panel background for every hue.

The one thing to verify yourself is the **FAB against your own page background** —
it sits on your site, outside the widget's themed surface.

Quick contrast check (optional):

```js
// Paste in DevTools to check contrast
function contrast(a, b) {
  // WCAG relative luminance formula
  const lum = (hex) => {
    const [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
    const lin = (c) => (c < 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
    return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
  };
  const [la, lb] = [lum(a), lum(b)].sort((x, y) => y - x);
  return ((la + 0.05) / (lb + 0.05)).toFixed(2);
}
contrast('#ff3e00', '#ffffff'); // 3.79 — OK for large text, fail for small
```

---

## Launcher

The floating bubble + its nudges. `floating` mode only.

### Variants

```ts
init({ launcher: { variant: 'circle' } }); // default — icon only, round
init({ launcher: { variant: 'pill', label: 'Ask AI' } }); // rounded rectangle
init({ launcher: { variant: 'bar', label: 'Need help?' } }); // wider rectangle
init({ launcher: { variant: 'minimal', label: 'Chat' } }); // text-only
```

| Variant   | Visual                 | Best for                       |
| --------- | ---------------------- | ------------------------------ |
| `circle`  | 56px round bubble      | Classic support chat           |
| `pill`    | Icon + label rounded   | "Ask AI" marketing CTA         |
| `bar`     | Wider prominent bar    | High-intent pages              |
| `minimal` | Transparent, text-only | Blends into custom site chrome |

### Sizes

```ts
init({ launcher: { size: 'sm' } }); // 44px — compact
init({ launcher: { size: 'md' } }); // 56px — default
init({ launcher: { size: 'lg' } }); // 68px — prominent
```

### Icons

```ts
init({ launcher: { icon: 'chat' } }); // default — speech bubble
init({ launcher: { icon: 'message' } }); // message square
init({ launcher: { icon: 'bubbles' } }); // stacked chat bubbles
init({ launcher: { icon: 'spark' } }); // sparkle / magic
init({ launcher: { icon: 'wand' } }); // wand / magic
init({ launcher: { icon: 'bot' } }); // robot face
init({ launcher: { icon: 'help' } }); // question mark
init({ launcher: { icon: 'lifebuoy' } }); // support ring
init({ launcher: { icon: 'headset' } }); // support agent
init({ launcher: { icon: 'search' } }); // friendly magnifier (search + smile)
init({ launcher: { icon: 'sparkles' } }); // multi-point sparkles (AI / magic)
init({ launcher: { icon: 'concierge' } }); // concierge bell (front-desk)
init({ launcher: { icon: 'wave' } }); // waving hand (say hi)
```

Or pass raw SVG source for a fully custom icon:

```ts
init({
  launcher: {
    icon: `<svg viewBox="0 0 24 24"><path d="M..." /></svg>`,
  },
});
```

SVG must be 24×24 viewbox with currentColor strokes/fills.

### Callout

A single pre-engagement bubble next to the FAB. One component,
configured for the use case — greeting, attention nudge, or
onboarding tip.

```ts
init({
  launcher: {
    callout: {
      text: "Hey! I'm Aria. What can I help with?",
      shape: 'pill', // pill / bubble / callout
      position: 'auto', // auto / horizontal / vertical
      delayMs: 1200,
      autoDismissMs: 0, // 0 = never (dismiss on click). Or 5000 for auto-dismiss after 5s.
      animated: true, // subtle horizontal nudge animation
      persistent: true, // default — re-show on every visit (greeting behaviour)
    },
  },
});
```

#### Dismissal lifecycles

`persistent` is the only behavioural difference between the two
typical patterns:

- **Recurring greeting (`persistent: true`, default)** — the bubble
  re-shows on every visit regardless of prior dismissal. Use for
  warm "Hi! 👋" copy that should always greet visitors.
- **One-time hint (`persistent: false`)** — the exact text is
  persisted to localStorage. Once the user clicks the X (or opens
  the widget once), the same copy stays hidden across visits.
  Reconfiguring with NEW text bypasses the prior dismissal — a new
  message is a new ask.

Pick `persistent: false` when the bubble is a CTA you don't want to
nag with after the first dismissal.

Tone: short, conversational. Think "Hi! 👋" or "Click here →" — not
"CLICK HERE FOR DISCOUNTS".

### Hiding the launcher entirely

Want a custom "open chat" button elsewhere on your site?

```ts
const chat = init({
  publicKey: 'agent_pk_...',
  aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
  launcher: { hidden: true },
});

// Your button
document.querySelector('#my-chat-btn').onclick = () => chat.open();
```

---

## Composer

The input area.

### Send button

```ts
init({
  composer: {
    sendButton: {
      shape: 'pill', // circle / square / pill
      icon: 'arrowUp', // arrowUp / paperPlane / send / chevron
      variant: 'filled', // filled / outline / ghost
    },
  },
});
```

3 × 4 × 3 = 36 combinations. Pick what matches your brand.

Quick recipes:

- **Minimal outline** — `{ shape: 'circle', variant: 'outline', icon: 'arrowUp' }`
- **Bold filled** — `{ shape: 'pill', variant: 'filled', icon: 'paperPlane' }`
- **Ghost text** — `{ shape: 'square', variant: 'ghost', icon: 'send' }`

### Disclaimer line

Below the textarea, small grey text. Classic "AI can make mistakes"
pattern.

```ts
init({ footer: { composerDisclaimer: 'AI-generated. Verify important details.' } });
```

Empty string = hidden. Keep it short (< 60 chars for mobile).

### Powered-by row

Optional attribution below the disclaimer.

```ts
init({
  footer: {
    poweredBy: {
      text: 'Powered by Help AI',
      href: 'https://help.ai',
      logoUrl: 'https://cdn.help.ai/logo.svg',
    },
  },
});
```

- Omitted fields inherit the active brand's attribution defaults. Set
  `footer.poweredBy.text` to `""` (without a logo) to hide the row.
- `href` wraps the row in a link with `target="_blank"
  rel="noopener noreferrer"`.

---

## Header actions

See [Configuration § Header actions](/sdk/configuration#header-actions)
for the full semantics. Quick customization examples:

### Minimal chrome

Strip everything but close:

```ts
init({
  header: { actions: ['close'] },
});
```

### Language-enabled

Enable the language row for multilingual marketing. Placement and ordering are
fixed by the widget; the array is only an enabled set.

```ts
init({
  i18n: { availableLocales: ['en', 'fr', 'es', 'de'] },
  header: { actions: ['language', 'theme', 'textSize', 'history', 'close', 'clear', 'expand', 'fullscreen', 'sound'] },
});
```

### History-focused chrome

Keep the header focused on history and basic preferences:

```ts
init({
  header: { actions: ['history', 'close', 'clear', 'sound', 'language', 'theme'] },
  features: { disableFileUpload: true, voice: 'disabled' },
});
```

---

## Internationalization

### Language switcher

Enable the `language` action — when more than one locale is available, the
header's ⋯ menu shows one segmented language row:

```ts
init({
  i18n: {
    availableLocales: ['en', 'fr', 'es', 'de', 'ja'],
    defaultLocale: 'auto', // sniff navigator.language
  },
  header: { actions: ['language', 'theme', 'history', 'close'] },
});
```

Locale display names come from `Intl.DisplayNames` in the active
locale — so a French user sees "anglais, français, espagnol", not
"English, French, Spanish".

The user's pick persists across devices (via the backend).

### Strings overrides

Every UI string is configurable. Two shapes:

**Flat** (applies to active locale):

```ts
init({
  i18n: {
    strings: {
      composerPlaceholder: 'Ask me anything…',
      send: 'Go',
      newConversation: 'Start over',
    },
  },
});
```

**Nested** (per-locale):

```ts
init({
  i18n: {
    strings: {
      en: { send: 'Go', composerPlaceholder: 'Ask me anything…' },
      fr: { send: 'Envoyer', composerPlaceholder: 'Demandez-moi…' },
      es: { send: 'Enviar', composerPlaceholder: 'Pregúntame…' },
    },
  },
});
```

### Full list of string keys

```
attach, attachmentMimeRejected, attachmentTooLarge,
attachmentTooMany, attachmentUploadFailed, conversationClosed, close, collapse,
composerPlaceholder, dateLastWeek, dateOlder, dateToday,
dateYesterday, dropZone, dropZoneRejected, errorGeneric, errorRetry, expand,
exitFullscreen, fullscreen, history, historyBack, historyEmpty,
historyLoading, historyTitle, language, launcherOpen,
conversationLoading, loadMoreConversations, loadPreviousMessages, micStart, micStop, micUnsupported, moreActions,
newConversation, panelTitle, resizeHandle, send, soundOff,
soundOn, startNewConversation, stop, theme, themeAuto, themeDark,
themeLight, thinking, usedTool
```

Any key you don't override keeps its default English value.

---

## Dashboard vs client-side

**Every theming option above can also be set in your Help AI
dashboard.** The dashboard lets non-engineers tune colors, copy, and
launcher chrome without redeploying your site.

Precedence:

1. Options you pass to `init()` win.
2. Dashboard settings fill in anything you leave unset.
3. Built-in defaults are the final fallback.

This means you can:

- Hardcode brand colors in code (so they never drift even if someone
  edits the dashboard).
- Let the dashboard own launcher copy (so marketing can A/B-test
  without asking engineering).
- Mix freely — every option is independently controlled.

---

## CSS-level customization (advanced)

The widget renders inside a shadow root, so your page CSS can't reach
it. If you need to go beyond the `theme` overrides, the widget exposes
~40 CSS custom properties on its host element you can tweak directly:

```css
web-ai-chat,
helpai-chat-host {
  /* Colors */
  --helpai-bg: #fafafa;
  --helpai-fg: #111;
  --helpai-border: #e5e5e5;
  /* Set the accent via the `-user` slots — the widget DERIVES the theme-adapted
     `--helpai-accent` / `--helpai-accent-text` / `--helpai-accent-fg` from these,
     so overriding the derived tokens directly has no effect on modern browsers. */
  --helpai-accent-user: #ff3e00;
  --helpai-accent-text-user: #fff;

  /* Panel dimensions */
  --helpai-panel-w: 420px;
  --helpai-panel-h: 640px;
  --helpai-expanded-w: 640px;
  --helpai-expanded-h: 820px;

  /* Launcher */
  --helpai-fab-size: 56px;

  /* Typography */
  --helpai-font: "Inter", system-ui, sans-serif;
  --helpai-radius: 10px;

  /* Motion */
  --helpai-dur-base: 240ms;
  --helpai-dur-quick: 120ms;
}
```

These cascade through the shadow root — a single rule on the host
element propagates to every component inside. Full list in
`src/styles/tokens.css` in the source.

### Styling parts

Five elements carry `part` names, so your own CSS restyles them through the shadow root with `::part()` — no
reaching into the widget, and names that stay stable across releases:

| Part       | Element                                                                    |
| ---------- | -------------------------------------------------------------------------- |
| `launcher` | The floating launcher button                                               |
| `panel`    | The open panel (chat, tabbed messenger or empty state)                     |
| `header`   | The bar at the top of the panel (the chat, history and sub-screen headers) |
| `messages` | The scrolling message list                                                 |
| `composer` | The message box area                                                       |

```css
web-ai-chat::part(launcher) {
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.25);
}
web-ai-chat::part(panel) {
  border-radius: 20px;
}
web-ai-chat::part(header) {
  border-bottom-color: transparent;
}
```

Use `helpai-chat-host::part(…)` for a widget mounted with `init()` / the script tag. Parts don't cross an iframe, so
`presentation.iframe: true` widgets are styled with tokens only. Prefer the tokens above for colour and size; parts
are for what tokens don't cover (shadows, corners, borders).

For truly bespoke designs (e.g., replacing the launcher with your
own component), use `launcher.hidden: true` + custom trigger, or a
[no-script trigger](/sdk/api-reference#no-script-triggers).

---

## Per-deployment vs per-instance

Some knobs are set at the **deployment** level (in your Help AI
dashboard) and apply to every widget mounted for that deployment:

- Theme defaults
- Launcher chrome defaults
- Welcome messages + suggestion chips
- Agent persona (name, avatar, status)
- Model routing
- Tool allowlist
- Action layout defaults
- I18n strings

Other knobs are best set **per-instance** in your `init()` call:

- `widgetId` (for multi-widget pages)
- `target` (for inline/standalone)
- `user` identity
- `onMessage` / `onOpen` / `onError` callbacks

Any option set in `init()` wins over the deployment default, so you
can always override.

---

## Next

- [Events & hooks](/sdk/events-and-hooks) — listening to chat
  interactions, integrating with analytics.
- [Recipes](/sdk/recipes) — copy-paste examples for common
  customization scenarios.
- [Configuration reference](/sdk/configuration) — the full option
  table.
