Theming
Colors, shapes, fonts, launcher styles, theme: "host" and ::part() styling.
Last updated
Make the widget match your brand — colors, shapes, typography, and launcher style.
TL;DR#
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, adark/lightclass on<html>or<body>, adata-theme,data-color-scheme,data-modeordata-bs-themeattribute there, then the page'scolor-scheme, then the OS.
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 overridesA 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:
init({
header: {
actions: ['textSize', 'history', 'close', 'clear', 'expand', 'fullscreen', 'sound', 'language'],
},
});Color overrides#
Theme properties you can override:
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
accentTextis 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):
// 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 smallLauncher#
The floating bubble + its nudges. floating mode only.
Variants#
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-onlySizes#
init({ launcher: { size: 'sm' } }); // 44px — compact
init({ launcher: { size: 'md' } }); // 56px — default
init({ launcher: { size: 'lg' } }); // 68px — prominentIcons#
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:
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.
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?
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#
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.
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.
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.textto""(without a logo) to hide the row. hrefwraps the row in a link withtarget="_blank" rel="noopener noreferrer".
Header actions#
See Configuration § Header actions for the full semantics. Quick customization examples:
Minimal chrome#
Strip everything but close:
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.
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:
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:
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):
init({
i18n: {
strings: {
composerPlaceholder: 'Ask me anything…',
send: 'Go',
newConversation: 'Start over',
},
},
});Nested (per-locale):
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, usedToolAny 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:
- Options you pass to
init()win. - Dashboard settings fill in anything you leave unset.
- 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:
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:
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.
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)useridentityonMessage/onOpen/onErrorcallbacks
Any option set in init() wins over the deployment default, so you
can always override.
Next#
- Events & hooks — listening to chat interactions, integrating with analytics.
- Recipes — copy-paste examples for common customization scenarios.
- Configuration reference — the full option table.