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-stateattributes
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
loaderUrlwe 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
publicChatbotIdwith 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
DOMContentLoadedfires. - 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-buttonor 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:
- Reads
window.babelbeezConfig(including yourcustomTriggerSelector) - If
customTriggerSelectoris set, it skips injecting our default button CSS - 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
- Does
- On click, it:
- Unlocks audio in a user gesture (Safari‑safe)
- Calls your project’s
/initialize-chatproxy - Starts the OpenAI Realtime / WebRTC session
- Continuously updates
data-stateand 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-stateand 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 yetloading– connecting / initializing sessionactive– connected and listeningspeaking– AI is speakingrag-retrieval– performing a knowledge base / RAG lookuperror– something went wrong (will auto‑revert toidleafter 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-labelwill change to things like:End AI voice chatAI is speakingSearching 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>, addrole="button"andtabindex="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:
Prefer server‑rendered / early‑mounted triggers
- Render your trigger in the base HTML template (or a layout that’s present from the first paint).
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.
- Today the recommended approach is to ensure your trigger is present by
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
customTriggerSelectoris missing or incorrect. - Check:
window.babelbeezConfig.customTriggerSelectoris 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.__bbIsSessionActiveisfalsebefore 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.
