Skip to main content

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 to HaloAgents.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, use openChat():

Attach to a custom button

In React:

Open and pre-fill a message

Open the chat and immediately send a message on behalf of the user:
Useful for contextual help links — e.g. a “Having trouble?” link next to a payment form that opens the chat with a billing-specific prompt.

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:
The SDK still loads, identifies the user, and tracks events. It renders no launcher-attached chrome, but dashboard-configured banners and in-app messages still show. Chat opens when you call 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.
This is the recommended approach inside authenticated app shells where the floating widget would conflict with your own UI. To suppress dashboard outreach as well (the old 0.5.1 through 0.8.x behavior), opt out explicitly:

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, use onChatOpen / onChatClose:
Both callbacks are idempotent: calling 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.
Earlier versions of this guide recommended observing .ab-trigger’s open class with a MutationObserver. That pattern is obsolete: in ui.trigger: false and ui: 'manual' mode there is no trigger element to observe. Use onChatClose instead.

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.), use manualOnPaths to switch into manual mode based on the current pathname. Patterns support * (any chars) and ? (one char) and are anchored to the full pathname.
Marketing pages get the default chrome (launcher, proactive teasers, greeting). On /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.
When a path matches a manual-mode pattern, the manual preset applies first. A 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 matching onBeforeShow* callback:
Each callback runs synchronously the moment its surface is about to render. Return 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:
Key points:
  • #haloagents-widget is the only element in your regular DOM. Target it with document.getElementById("haloagents-widget") or CSS #haloagents-widget.
  • All internal elements live inside a Shadow DOM and aren’t accessible via normal document.querySelector(). Use document.getElementById("haloagents-widget")?.shadowRoot?.querySelector(...) instead.
  • The Shadow DOM uses mode: "open", so you can access internal elements via the shadowRoot property — 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 support is, 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):
This keeps the widget initialized — conversations and event tracking continue. Just not visible.

Toggle visibility dynamically

Hide only the trigger button

Use ui.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:
Pair with your own button:
Earlier versions of this guide recommended hiding .ab-trigger and other internal classes via Shadow DOM CSS injection. That approach is brittle: every new UI primitive we ship can leak through and intercept clicks. Use ui config (above) instead.

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 for onChatClose:
To remember the dismissal across page loads:
Use localStorage instead of sessionStorage to persist across browser sessions.
Requires SDK 0.7+. On older versions, observe .ab-trigger’s open class with a MutationObserver as a fallback:
This fallback only works when the default trigger is present; in 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:
To bring it back, call 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

Wrap HaloAgents.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:
The AI uses smart selector strategies and often finds elements even without perfect IDs, but stable selectors significantly improve reliability.

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.