Overview
The Halo widget is fully controllable from your code. You can hide the default trigger button, open the chat from your own UI, adjust positioning and theming, and use the built-in UI highlighting system to walk users through your product step-by-step.Configuration reference
Pass an options object toHaloAgents.init():
Required
Your organization ID.
User identification
The current user’s unique identifier in your system. Set to
"anonymous" or omit for unauthenticated users.The current user’s company identifier.
Key-value attributes for the current user. See User Traits.
Key-value attributes for the user’s company. See Company Traits.
Structured context entries. See Context Entries.
Connection & authentication
Publishable widget key (
ab_live_...). Safe to expose client-side. Copy from Setup > Install.JWT signed server-side using your Identity Secret (HS256) for identity verification. Required when identity verification is enabled. See Identity Verification.
Async callback that fetches a fresh JWT from your backend when
userToken is missing or expired. Preferred over manual rotation. SDK 0.9.0+.Fires once when a JWT expires, is missing, or is rejected (403). User-scoped SDK requests pause until
getUserToken or identify(..., { userToken }) succeeds. SDK 0.9.0+.Base URL of the Halo API. Only set if self-hosting.
Appearance
Position of the chat widget trigger button.
Widget color theme.
"auto" matches the user’s OS preference.The initial mode when the widget opens.
'custom' is for organizations that have configured a custom widget mode in the dashboard.UI surface control
Suppress launcher-attached UI surfaces. Pass
'manual' for host-controlled chat entry (no floating trigger, no proactive teasers, no greeting). Dashboard-configured banners and in-app messages still render unless you opt out. Pass an object for per-surface control. See Manual mode and per-surface control.Glob patterns matched against
window.location.pathname. When the current path matches, the widget applies the manual preset (hidden launcher, dashboard outreach still on by default). Pass a ui object alongside to opt out of banners or in-app messages on matched paths. Useful when one init() call serves both marketing pages and an authenticated app.Called synchronously before each proactive teaser is shown. Return
false to veto this specific message.Called synchronously before each top banner is shown. Return
false to veto. Useful when a particular page renders its own header content that would collide with a banner.Called synchronously before each in-app message overlay is shown. Return
false to veto. Useful for the “modal collision” case: the host already has a dialog open and doesn’t want the SDK’s centered overlay landing on top.Callbacks
Called when the widget has mounted and is ready.
Called when an error occurs.
Called when a new message is sent or received.
Called when the AI triggers an action (highlight, ticket creation, etc.).
Called when the chat panel transitions from closed to open. Fires regardless of who initiated the open: host code calling
openChat(), the user tapping the floating trigger, or the SDK auto-opening on unseen messages. Idempotent: calling openChat() while already open does not fire this callback again.Primary use case: keep your own “chat open” state in sync with the panel when you’ve hidden the SDK trigger (ui.trigger: false or ui: 'manual').Called when the chat panel transitions from open to closed. Fires regardless of who initiated the close: host code calling
closeChat(), the user tapping the panel’s close button, or programmatic dismissal. Idempotent: calling closeChat() while already closed does not fire this callback again.This is the supported replacement for the pre-0.7 MutationObserver on .ab-trigger pattern, which does not work in ui.trigger: false mode because there is no trigger element to observe.Opening the widget programmatically
If you want to trigger the chat from your own button or in-app event instead of the default floating trigger, useopenChat():
Attach to a custom button
Open and pre-fill a message
Open the chat and immediately send a message on behalf of the user:Manual mode and per-surface control
If you want chat available only when the user clicks your own button, with no floating launcher, no proactive teasers, and no greeting popup, use manual mode:openChat():
In-app messages require
init() on page load, not deferred until the user clicks your button. If you only call init() when Contact Support is clicked, no in-app message can appear until then.Tracking open state in manual mode
When the trigger is hidden, the user can still close the panel from the X button inside it. To keep your own UI (button label, focus state, mobile sheet, analytics) in sync, useonChatOpen / onChatClose:
openChat() / closeChat() while the panel is already in that state does not fire them again. Throws inside your callback are caught and routed through onError (if set), so a bug in your handler can never desync the SDK’s open/close cycle.
Per-surface flags
For finer control, pass an object instead of'manual'. Any omitted key falls back to the default (visible). SDK-level flags can only restrict what the dashboard allows, never expand it.
| Flag | What it suppresses |
|---|---|
trigger | The floating launcher button (.ab-trigger). Implies badge: false and greeting: false. |
badge | The unseen-message count badge on the trigger. |
proactive | All proactive teasers (time, scroll, exit-intent) configured in the dashboard. |
greeting | The auto-show greeting popup tethered to the trigger. |
autoOpenUnseen | The auto-open behavior when an agent has new messages waiting. The badge still appears (unless badge: false). |
banners | Top banner messages configured under Active Messages in the dashboard (full-width pinned to the top of the page). |
inAppMessages | In-app message overlays configured under Active Messages in the dashboard (centered modal or anchored card). |
Mixing marketing and app pages in one init() call
When the same root layout serves both your marketing site and your authenticated app (common in Next.js, Remix, etc.), usemanualOnPaths to switch into manual mode based on the current pathname. Patterns support * (any chars) and ? (one char) and are anchored to the full pathname.
/app/... and /forms/... the widget is in manual mode: no launcher, but banners and in-app messages still render unless you opt out. Chat opens when you call openChat(). The user is identified across all pages.
ui object merges on top so you can opt out of specific surfaces on those paths.
manualOnPaths is evaluated once when init() runs. If your app does in-page navigation between matched and unmatched paths without a hard reload, call destroy() and re-init to re-evaluate.Vetoing individual messages
If you want a surface enabled globally but need to suppress specific messages based on page state (e.g. one of your modals is open and would collide with the SDK’s overlay), use the matchingonBeforeShow* callback:
false to suppress that specific message; any other return value (including true, undefined, or a thrown exception) allows it.
Hiding the widget
Several ways to hide the widget depending on your use case.Widget DOM structure
The widget creates a single host element with all internal UI inside a Shadow DOM:#haloagents-widgetis the only element in your regular DOM. Target it withdocument.getElementById("haloagents-widget")or CSS#haloagents-widget.- All internal elements live inside a Shadow DOM and aren’t accessible via normal
document.querySelector(). Usedocument.getElementById("haloagents-widget")?.shadowRoot?.querySelector(...)instead. - The Shadow DOM uses
mode: "open", so you can access internal elements via theshadowRootproperty — they’re just isolated from your page’s CSS.
Observable widget states
| What you want to know | How to detect it |
|---|---|
| Widget exists on the page | document.getElementById("haloagents-widget") !== null |
| Chat panel opened | onChatOpen callback (or .ab-trigger.open class when the trigger is visible) |
| Chat panel closed | onChatClose callback (or .ab-trigger without the open class when the trigger is visible) |
| Widget is in dark mode | Host element has class dark |
| Widget is on the left side | .ab-trigger has class left |
ui.trigger: false and ui: 'manual' mode (where no trigger element exists), and don’t require parsing internal class names that may change between releases.
Method 1: Page URL filters (no code)
Hide the widget on specific pages using Page URL Rules on the agent’s chat widget channel: AI Agents > [Agent] > Channels > Chat Widget > Configure > Page URL Rules. When a page doesn’t match, the widget is fully unmounted, no DOM element at all. Rules supportis, is_not, contains, starts_with, ends_with against both full URL and pathname.
Method 2: Hide with CSS
Target the host element to hide it entirely (trigger + panel):Toggle visibility dynamically
Hide only the trigger button
Useui.trigger: false (or ui: 'manual') at init time. This is the supported API and won’t break when we add new UI primitives in future releases:
Responsive hiding
Method 3: Hide when the user closes the chat
By default, when a user closes the chat, only the panel collapses, the trigger stays visible. To hide the entire widget the moment the user closes the panel, listen foronChatClose:
localStorage instead of sessionStorage to persist across browser sessions.
Requires SDK 0.7+. On older versions, observe This fallback only works when the default trigger is present; in
.ab-trigger’s open class with a MutationObserver as a fallback:ui.trigger: false or ui: 'manual' mode there is no trigger to observe and onChatClose is required.Method 4: Destroy and reinitialize
To completely remove the widget from the DOM:HaloAgents.init() again.
Use for SPAs where the widget should only appear in certain sections, on logout, or to free resources.
Method 5: Conditional initialization
WrapHaloAgents.init() in a condition:
Comparison
| Method | Widget in DOM | Stays initialized | Instant reopen | No code |
|---|---|---|---|---|
ui: 'manual' / per-surface flags | Yes (no chrome) | Yes | Yes | No |
manualOnPaths | Yes (chrome on non-matching paths) | Yes | Yes | No |
| Page URL filters | No | No | No | Yes |
CSS display: none | Yes (hidden) | Yes | Yes | No |
| Hide on close | Yes (hidden after close) | Yes | Yes | No |
destroy() | No | No | No | No |
| Conditional init | No | No | No | No |
ui config (the first two rows). It’s typed, future-proof, and survives DOM rerenders.
Theming
Three theme modes:| Value | Behavior |
|---|---|
"auto" | Matches the user’s OS preference (default) |
"light" | Always light mode |
"dark" | Always dark mode |
/dashboard/settings/widget. (This page isn’t listed in the Settings sidebar, navigate to it directly or follow links from the agent’s chat widget channel.)
Trigger icon
The Widget Appearance page exposes a preset grid of trigger icons. Current options include:- Chat Bubble (default), Message, Conversation, Send
- Headset, Microphone, Phone, Audio
- Sparkles, Help Circle, Life Buoy, Info, Shield
- Robot, Zap, Heart, Star, Wave
UI Highlighting
Halo can highlight elements in your UI to visually guide users through workflows. The AI triggers highlights automatically when walking a user through your product, but you can also trigger them manually.How it works
When the AI needs to point at a specific UI element (“Click the Settings button”), it uses CSS selectors to find and highlight the element. An animated overlay appears around the target.Manual highlighting
Highlight styles
Configure on the UI Highlighting tab of the Widget Appearance page (/dashboard/settings/widget):
| Style | Description |
|---|---|
| Glow Shadow | A soft glowing drop shadow radiating from the element |
| Pulsing Outline | A colored outline that gently pulses around the element |
| Spotlight | Dims the rest of the page and spotlights the element |
| Bounce Arrow | An animated arrow bouncing above the element |
| Outline + Arrow | Pulsing outline combined with a bouncing arrow pointer |
Best practices
For the AI to highlight elements reliably, use stable selectors:Callbacks
React to widget events in your app:onChatOpen and onChatClose are the supported way to keep your own open-state in sync when you’ve hidden the SDK trigger. They fire once per state transition and are isolated from host throws.
Where to go next
Methods Reference
Every method available on the HaloAgents instance.
Identify Users
Pass user and company data to the AI.
Send Context
Push structured user state.