Skip to content

Headless embed: use your own chat button

This guide is for web agencies and developers who want full control over the chat trigger’s design and markup.

Headless mode lets you completely design your own trigger button (or icon, link, etc.) while keeping all of Babelbeez’s audio, session, and analytics logic.

Instead of using our default floating circle, you:

  • Keep full control over markup and styling
  • Point Babelbeez at a CSS selector for your trigger element
  • Let our script drive its state via data attributes (for example data-state="loading", data-state="speaking")

When to use headless mode

Use headless mode if you:

  • Want the chat trigger to match an existing design system (your own CTA, icon, or FAB)
  • Need to control positioning and layout yourself (navbar, sticky footer, floating widget, etc.)
  • Are comfortable writing a bit of CSS that reacts to data-state attributes

If you’re happy with our default round floating button, you can keep using the regular embed and skip this page.


Step 1 – Add the standard embed snippet

Headless mode still uses the same embed snippet that you get from the dashboard. The only difference will be an extra customTriggerSelector field that we add later.

A typical snippet looks like this (note the inline loader boot script):

html
<!-- Babelbeez Chatbot Embed Start -->
<script>
  window.babelbeezConfig = {
    publicChatbotId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", // example – replace with your own ID
    proxyInitializeUrl: "https://www.babelbeez.com/initialize-chat",
    loaderUrl: "https://www.babelbeez.com/assets/embedLoader.js"
  };
  (function() {
    var d = document;
    var s = d.createElement('script');
    s.src = window.babelbeezConfig.loaderUrl;
    s.async = true;
    d.body.appendChild(s);
  })();
</script>
<!-- Babelbeez Chatbot Embed End -->

Notes:

  • Always use the loaderUrl we provide (do not change it).
  • Keep the IIFE that creates and appends the <script> element – that’s how the loader is actually pulled in.
  • Replace the example publicChatbotId with the one from your own dashboard.

Place this snippet just before </body> on every page where you want the voice agent.


Step 2 – Add a custom trigger element

Next, add your own element anywhere in your layout. It can be a <button>, <div>, <a>, an icon wrapper, etc.

Example: simple button in the bottom‑right

html
<button class="bb-headless-trigger" type="button">
  Talk to us
</button>

You can also use something like a floating action button, an icon in your navbar, or a support badge—headless mode doesn’t care.

Important:

  • The element must exist by the time DOMContentLoaded fires.
  • If you create it later via client‑side JS, see Advanced: late‑mount triggers below.

Step 3 – Enable headless mode with customTriggerSelector

To tell Babelbeez to “attach” to your own element instead of rendering our default floating button, you add a customTriggerSelector field to window.babelbeezConfig.

Here’s the same embed snippet as above, now with headless mode enabled:

html
<!-- Babelbeez Chatbot Embed Start -->
<script>
  window.babelbeezConfig = {
    publicChatbotId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", // example – replace with your own ID
    proxyInitializeUrl: "https://www.babelbeez.com/initialize-chat",
    loaderUrl: "https://www.babelbeez.com/assets/embedLoader.js",

    // Headless mode: use my own trigger element
    customTriggerSelector: ".bb-headless-trigger"
  };
  (function() {
    var d = document;
    var s = d.createElement('script');
    s.src = window.babelbeezConfig.loaderUrl;
    s.async = true;
    d.body.appendChild(s);
  })();
</script>
<!-- Babelbeez Chatbot Embed End -->

When customTriggerSelector is present:

  • We do not inject our base button CSS
  • We do not create #babelbeez-chat-button or the default wrapper
  • We still create a hidden chat container (#babelbeez-chat-container) when the session starts, which powers the transcript and status UI

How headless mode behaves under the hood

On page load, our embed loader:

  1. Reads window.babelbeezConfig (including your customTriggerSelector)
  2. If customTriggerSelector is set, it skips injecting our default button CSS
  3. On DOMContentLoaded, it:
    • Does document.querySelector(customTriggerSelector)
    • Stores that element as its internal chatUiButton
    • Attaches the click handler that starts the voice session
    • Ensures there is an initial data-state="idle" if you haven’t set one
  4. On click, it:
    • Unlocks audio in a user gesture (Safari‑safe)
    • Calls your project’s /initialize-chat proxy
    • Starts the OpenAI Realtime / WebRTC session
    • Continuously updates data-state and ARIA attributes on your trigger as the session runs

All of the audio, inactivity handling, human handoff, and backend session tracking are identical to the standard embed.

Your job as an implementer is simply:

  • Provide a valid selector that resolves to exactly one element
  • Style that element based on the data-state and ARIA attributes we set

Step 4 – Style your trigger using data-state

Our loader and session code drive your element’s data-state throughout the lifecycle.

These are the main states you should handle in CSS:

  • idle – agent is ready, no active call yet
  • loading – connecting / initializing session
  • active – connected and listening
  • speaking – AI is speaking
  • rag-retrieval – performing a knowledge base / RAG lookup
  • error – something went wrong (will auto‑revert to idle after a short delay)

We set this on your element as:

html
<button class="bb-headless-trigger" data-state="idle">Talk to us</button>

Then update it as the session runs.

Example CSS

css
.bb-headless-trigger {
  position: fixed;
  right: 24px;
  bottom: 24px;
  padding: 14px 20px;
  border-radius: 999px;
  border: none;
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease;
}

.bb-headless-trigger[data-state="idle"] {
  background-color: #10b981;
  color: white;
  box-shadow: 0 8px 20px rgba(16, 185, 129, 0.4);
}

.bb-headless-trigger[data-state="loading"] {
  background-color: #6b7280;
  color: white;
  cursor: wait;
}

.bb-headless-trigger[data-state="active"],
.bb-headless-trigger[data-state="speaking"] {
  background-color: #0f766e;
  color: white;
}

.bb-headless-trigger[data-state="rag-retrieval"] {
  background: linear-gradient(90deg, #0f766e, #0284c7);
  color: white;
}

.bb-headless-trigger[data-state="error"] {
  background-color: #ef4444;
  color: white;
}

.bb-headless-trigger:hover {
  transform: translateY(-1px);
}

.bb-headless-trigger:active {
  transform: translateY(0);
}

You’re free to design completely differently—the only contract is the data-state values.


Step 5 – Optional: react to dynamic labels (data-label)

Our loader can also set a data-label attribute, based on:

  • Public appearance settings (from your Babelbeez dashboard)
  • Per‑chatbot behavior such as a multilingual “hello” cycler

For example, depending on configuration we might set:

html
<button class="bb-headless-trigger"
        data-state="idle"
        data-label="Hello">
</button>

In headless mode, we don’t inject any HTML inside your element, so if you want to use the label you can bind it via CSS or a small bit of JS:

CSS example using ::after:

css
.bb-headless-trigger::after {
  content: attr(data-label);
}

If you prefer to fully control text/icon yourself, you can ignore data-label completely.


Accessibility behavior

We also keep ARIA attributes in sync for screen readers:

  • When idle:
    • aria-pressed="false"
    • aria-label="Start AI voice chat"
  • When active / speaking / retrieving:
    • aria-pressed="true"
    • aria-label will change to things like:
      • End AI voice chat
      • AI is speaking
      • Searching knowledge base...

This works even if your trigger is a <div> or <a>—we still set the attributes, and you can add role="button" yourself if appropriate.

Recommendation:

  • If you’re not using a <button>, add role="button" and tabindex="0" so the element is keyboard‑focusable.
html
<div class="bb-headless-trigger"
     role="button"
     tabindex="0">
  Talk to us
</div>

You can also wire Enter/Space keypresses to trigger a click if you’re building a fully custom control.


Advanced: late‑mount triggers (single‑page apps)

The loader currently looks up your custom trigger once on DOMContentLoaded:

js
const customEl = document.querySelector(config.customTriggerSelector);

If you’re using a SPA framework (React, Vue, etc.), and the element is mounted after DOMContentLoaded, the loader will not find it and will fall back to the default floating button.

Options:

  1. Prefer server‑rendered / early‑mounted triggers

    • Render your trigger in the base HTML template (or a layout that’s present from the first paint).
  2. Manually re‑attach after mount (advanced)

    • Today the recommended approach is to ensure your trigger is present by DOMContentLoaded. If you need a reusable helper for a specific framework, reach out to us and we can share an integration pattern.

If you’re unsure which category you’re in, start with a simple static trigger and verify that headless mode works, then integrate into your framework.


Troubleshooting headless mode

My custom button never appears

  • Make sure you’ve added the HTML for your trigger (we don’t create it for you in headless mode)
  • Confirm the selector is valid in the browser console:
    • Open DevTools → Console
    • Run: document.querySelector('YOUR_SELECTOR')
    • It should return your element, not null

I see both my button and the default Babelbeez round button

  • This happens if customTriggerSelector is missing or incorrect.
  • Check:
    • window.babelbeezConfig.customTriggerSelector is set before the loader script runs
    • The selector actually matches an element by DOMContentLoaded

States don’t change when I click

  • Check for JS errors in the console
  • Confirm that window.__bbIsSessionActive is false before clicking (DevTools → Console)
  • Add a temporary log inside your click handler to ensure:
    • Your element exists
    • The click is being received

If you see Babelbeez Loader: Headless mode active. Attached to ... in the console (development mode), the loader successfully attached to your trigger.

Audio / mic issues

Headless mode doesn’t change audio behavior. Use the same guides:


Summary

Headless mode gives you:

  • Full visual control over the chat trigger
  • A simple contract (customTriggerSelector + data-state)
  • The same audio, session, and analytics pipeline as the standard embed

Once you’ve set up your trigger and CSS, the rest of the experience—greeting, RAG retrieval, inactivity handling, human handoff, and end‑of‑session reporting—remains powered by Babelbeez.

Note: Headless mode applies to the trigger button and session states. Native UI elements such as the email handoff modal currently use the default Babelbeez styling.

© 2025 Babelbeez. All rights reserved.