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():
var ha = window.HaloAgents.init({
  orgId: "your-org-id",
  apiKey: "ab_live_xxxxxxxxxxxxxxxx",

  userId: "user_123",
  companyId: "company_456",

  userTraits: { name: "Jane Doe", email: "[email protected]" },
  companyTraits: { name: "Acme Corp", plan: "enterprise" },

  context: {
    integrations: {
      label: "Connected Integrations",
      type: "integration",
      value: { stripe: { connected: true, status: "active" } }
    }
  },

  position: "bottom-right",
  theme: "auto",
  defaultMode: "chat",

  onReady: () => console.log("Widget ready"),
  onError: (error) => console.error(error),
  onMessage: (message) => console.log("New message:", message),
  onAction: (action) => console.log("AI action:", action),
  onChatOpen: () => console.log("Chat opened"),
  onChatClose: () => console.log("Chat closed"),
});

Required

orgId
string
required
Your organization ID.

User identification

userId
string
The current user’s unique identifier in your system. Set to "anonymous" or omit for unauthenticated users.
companyId
string
The current user’s company identifier.
userTraits
UserTraits
Key-value attributes for the current user. See User Traits.
companyTraits
CompanyTraits
Key-value attributes for the user’s company. See Company Traits.
context
Record<string, ContextEntry>
Structured context entries. See Context Entries.

Connection & authentication

apiKey
string
Publishable widget key (ab_live_...). Safe to expose client-side. Copy from Setup > Install.
userToken
string
JWT signed server-side using your Identity Secret (HS256) for identity verification. Required when identity verification is enabled. See Identity Verification.
getUserToken
() => 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+.
onIdentityExpired
(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+.
apiUrl
string
default:"https://api.haloagents.ai"
Base URL of the Halo API. Only set if self-hosting.

Appearance

position
'bottom-right' | 'bottom-left'
default:"bottom-right"
Position of the chat widget trigger button.
theme
'light' | 'dark' | 'auto'
default:"auto"
Widget color theme. "auto" matches the user’s OS preference.
defaultMode
'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

ui
'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.
manualOnPaths
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.
onBeforeShowProactive
(message: string) => boolean
Called synchronously before each proactive teaser is shown. Return false to veto this specific message.
onBeforeShowBanner
(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.
onBeforeShowInAppMessage
(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

onReady
() => void
Called when the widget has mounted and is ready.
onError
(error: Error) => void
Called when an error occurs.
onMessage
(message: ChatMessage) => void
Called when a new message is sent or received.
onAction
(action: AgentAction) => void
Called when the AI triggers an action (highlight, ticket creation, etc.).
onChatOpen
() => 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').
onChatClose
() => 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():
var ha = window.HaloAgents.getInstance();
ha?.openChat();

Attach to a custom button

<button onclick="HaloAgents.getInstance()?.openChat()">
  Talk to Support
</button>
In React:
function SupportButton() {
  const handleClick = () => {
    HaloAgents.getInstance()?.openChat();
  };

  return <button onClick={handleClick}>Talk to Support</button>;
}

Open and pre-fill a message

Open the chat and immediately send a message on behalf of the user:
const ha = HaloAgents.getInstance();
ha?.openChat();
ha?.sendMessage("I need help with billing");
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:
HaloAgents.init({
  orgId: "your-org-id",
  apiKey: "ab_live_xxxxxxxxxxxxxxxx",
  ui: "manual",
});
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():
<button onclick="HaloAgents.getInstance()?.openChat()">Contact Support</button>
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:
HaloAgents.init({
  orgId: "your-org-id",
  apiKey: "ab_live_xxxxxxxxxxxxxxxx",
  ui: {
    trigger: false,
    proactive: false,
    inAppMessages: false,
    banners: false,
  },
});

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:
HaloAgents.init({
  orgId: "your-org-id",
  ui: "manual",
  onChatOpen: () => setIsChatOpen(true),
  onChatClose: () => setIsChatOpen(false),
});
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.
HaloAgents.init({
  orgId: "your-org-id",
  apiKey: "ab_live_xxxxxxxxxxxxxxxx",
  ui: {
    trigger: false,        // hide the floating launcher
    badge: false,          // hide the unseen-count badge
    proactive: false,      // disable proactive teasers
    greeting: false,       // disable the auto-show greeting popup
    autoOpenUnseen: false, // don't auto-open when an agent has unseen replies
    banners: false,        // hide top banner messages
    inAppMessages: false,  // hide in-app message overlays
  },
});
FlagWhat it suppresses
triggerThe floating launcher button (.ab-trigger). Implies badge: false and greeting: false.
badgeThe unseen-message count badge on the trigger.
proactiveAll proactive teasers (time, scroll, exit-intent) configured in the dashboard.
greetingThe auto-show greeting popup tethered to the trigger.
autoOpenUnseenThe auto-open behavior when an agent has new messages waiting. The badge still appears (unless badge: false).
bannersTop banner messages configured under Active Messages in the dashboard (full-width pinned to the top of the page).
inAppMessagesIn-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.), 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.
HaloAgents.init({
  orgId: "your-org-id",
  apiKey: "ab_live_xxxxxxxxxxxxxxxx",
  manualOnPaths: ["/app/*", "/forms/*"],
});
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.
HaloAgents.init({
  orgId: "your-org-id",
  apiKey: "ab_live_xxxxxxxxxxxxxxxx",
  manualOnPaths: ["/app/*", "/forms/*"],
  ui: { inAppMessages: false, banners: false }, // optional opt-out on matched paths
});
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:
HaloAgents.init({
  orgId: "your-org-id",
  apiKey: "ab_live_xxxxxxxxxxxxxxxx",
  onBeforeShowProactive: (message) => {
    return !document.querySelector('.modal-open');
  },
  onBeforeShowBanner: ({ id }) => {
    return !document.body.classList.contains('checkout');
  },
  onBeforeShowInAppMessage: ({ id }) => {
    return !document.querySelector('dialog[open]');
  },
});
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:
document.body
└── div#haloagents-widget              ← Host element (the only element in your DOM)
    └── #shadow-root (open)
        ├── <style>                    ← Scoped widget styles
        ├── button.ab-trigger          ← Floating trigger button
        │   └── (SVG icon)
        └── div.ab-panel               ← Chat panel
            ├── .ab-header
            │   ├── .ab-close-btn
            │   └── ...
            └── ...
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

What you want to knowHow to detect it
Widget exists on the pagedocument.getElementById("haloagents-widget") !== null
Chat panel openedonChatOpen callback (or .ab-trigger.open class when the trigger is visible)
Chat panel closedonChatClose callback (or .ab-trigger without the open class when the trigger is visible)
Widget is in dark modeHost element has class dark
Widget is on the left side.ab-trigger has class left
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):
#haloagents-widget {
  display: none !important;
}
This keeps the widget initialized — conversations and event tracking continue. Just not visible.

Toggle visibility dynamically

function hideWidget() {
  const el = document.getElementById("haloagents-widget");
  if (el) el.style.display = "none";
}

function showWidget() {
  const el = document.getElementById("haloagents-widget");
  if (el) el.style.display = "";
}

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:
HaloAgents.init({
  orgId: "your-org-id",
  ui: { trigger: false },
});
Pair with your own button:
<button onclick="HaloAgents.getInstance()?.openChat()">Need help?</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

@media (max-width: 768px) {
  #haloagents-widget { display: none !important; }
}

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:
HaloAgents.init({
  orgId: "your-org-id",
  onChatClose: () => {
    const host = document.getElementById("haloagents-widget");
    if (host) host.style.display = "none";
  },
});
To remember the dismissal across page loads:
HaloAgents.init({
  orgId: "your-org-id",
  onReady: () => {
    if (sessionStorage.getItem("haloagents-dismissed") === "true") {
      const host = document.getElementById("haloagents-widget");
      if (host) host.style.display = "none";
    }
  },
  onChatClose: () => {
    const host = document.getElementById("haloagents-widget");
    if (host) host.style.display = "none";
    sessionStorage.setItem("haloagents-dismissed", "true");
  },
});
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:
HaloAgents.init({
  orgId: "your-org-id",
  onReady: () => {
    const host = document.getElementById("haloagents-widget");
    const trigger = host?.shadowRoot?.querySelector(".ab-trigger");
    if (!trigger) return;
    const observer = new MutationObserver(() => {
      if (!trigger.classList.contains("open")) host.style.display = "none";
    });
    observer.observe(trigger, { attributes: true, attributeFilter: ["class"] });
  },
});
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:
HaloAgents.getInstance()?.destroy();
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:
if (shouldShowWidget()) {
  HaloAgents.init({ orgId: "your-org-id" });
}
useEffect(() => {
  if (user.plan === "pro") {
    HaloAgents.init({ orgId: "your-org-id", userId: user.id });
    return () => HaloAgents.getInstance()?.destroy();
  }
}, [user.plan]);

Comparison

MethodWidget in DOMStays initializedInstant reopenNo code
ui: 'manual' / per-surface flagsYes (no chrome)YesYesNo
manualOnPathsYes (chrome on non-matching paths)YesYesNo
Page URL filtersNoNoNoYes
CSS display: noneYes (hidden)YesYesNo
Hide on closeYes (hidden after close)YesYesNo
destroy()NoNoNoNo
Conditional initNoNoNoNo
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:
HaloAgents.init({
  orgId: "your-org-id",
  theme: "auto", // "light" | "dark" | "auto"
});
ValueBehavior
"auto"Matches the user’s OS preference (default)
"light"Always light mode
"dark"Always dark mode
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

const ha = HaloAgents.getInstance();

ha?.highlightElement("#settings-btn", {
  color: "#18181b",
  label: "Click here",
});

ha?.clearHighlights();

Highlight styles

Configure on the UI Highlighting tab of the Widget Appearance page (/dashboard/settings/widget):
StyleDescription
Glow ShadowA soft glowing drop shadow radiating from the element
Pulsing OutlineA colored outline that gently pulses around the element
SpotlightDims the rest of the page and spotlights the element
Bounce ArrowAn animated arrow bouncing above the element
Outline + ArrowPulsing outline combined with a bouncing arrow pointer
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:
<!-- Good -->
<button id="settings-btn">Settings</button>
<button data-action="export-csv">Export</button>

<!-- Avoid -->
<button class="btn-primary mt-2">Settings</button>
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:
HaloAgents.init({
  orgId: "your-org-id",
  onReady: () => console.log("Widget is ready"),
  onMessage: (message) => analytics.track("support_message", { text: message.text }),
  onAction: (action) => console.log("AI action:", action),
  onError: (error) => console.error("Widget error:", error),
  onChatOpen: () => setIsChatOpen(true),
  onChatClose: () => setIsChatOpen(false),
});
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.