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
string
required
Your organization ID.
User identification
string
The current user’s unique identifier in your system. Set to
"anonymous" or omit for unauthenticated users.string
The current user’s company identifier.
UserTraits
Key-value attributes for the current user. See User Traits.
CompanyTraits
Key-value attributes for the user’s company. See Company Traits.
Record<string, ContextEntry>
Structured context entries. See Context Entries.
Connection & authentication
string
Publishable widget key (
ab_live_...). Safe to expose client-side. Copy from Setup > Install.string
JWT signed server-side using your Identity Secret (HS256) for identity verification. Required when identity verification is enabled. See Identity Verification.
() => string | Promise<string | null | undefined>
Async callback that fetches a fresh JWT from your backend when
userToken is missing or expired. Preferred over manual rotation. SDK 0.9.0+.(info: IdentityExpiredInfo) => void
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+.string
default:"https://api.haloagents.ai"
Base URL of the Halo API. Only set if self-hosting.
Appearance
'bottom-right' | 'bottom-left'
default:"bottom-right"
Position of the chat widget trigger button.
'light' | 'dark' | 'auto'
default:"auto"
Widget color theme.
"auto" matches the user’s OS preference.'chat' | 'voice' | 'livehelp' | 'custom'
default:"chat"
The initial mode when the widget opens.
'custom' is for organizations that have configured a custom widget mode in the dashboard.UI surface control
'manual' | 'default' | UISuppression
default:"default"
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.string[]
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.(message: string) => boolean
Called synchronously before each proactive teaser is shown. Return
false to veto this specific message.(message: { id: string; body: string }) => boolean
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.(message: { id: string; body: string }) => boolean
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
() => void
Called when the widget has mounted and is ready.
(error: Error) => void
Called when an error occurs.
(message: ChatMessage) => void
Called when a new message is sent or received.
(action: AgentAction) => void
Called when the AI triggers an action (highlight, ticket creation, etc.).
() => void
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').() => void
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.
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
Prefer the callbacks. They fire on the actual transition, work in
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
For most “hide the launcher inside our app” use cases, prefer
ui config (the first two rows). It’s typed, future-proof, and survives DOM rerenders.
Theming
Three theme modes:
Customize colors, header text, and trigger icon on the Widget Appearance page at
/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):
You can also customize highlight color for light and dark themes, and the arrow indicator color.
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.