Facade loader
RecommendedRenders the chat bubble in ~2 KB. The full bubuu-chat.js bundle preloads in the background and is promoted on click or hover.
A 2 KB facade that paints on first frame. The full widget streams in behind it. Embedded into any page, framework, or CMS in two lines of HTML.
Bubuu's lead widget is a Custom Element — <bubuu-chat> — that drops a guided real-estate conversation into any page. It mounts inside a Shadow DOM, inheriting none of the host site's CSS and leaking none of its own. No frameworks to align, no global styles to fight.
The widget ships in two halves. A 2 KB loader renders the chat bubble on first paint; the full bundle preloads in the background and takes over on click. Visitors on cold connections still see the bubble before the page finishes settling.
Two scripts, one element. Drop the loader on every page where the widget should be reachable; the chat bubble renders immediately and the full widget streams in behind it.
Renders the chat bubble in ~2 KB. The full bubuu-chat.js bundle preloads in the background and is promoted on click or hover.
Use this when the page is a conversion target and you want the widget interactive on first paint. Ships the full ~210 KB bundle up-front — skip if you care about Core Web Vitals on cold visits.
Two scripts, in sequence. The loader is tiny enough to be inlined on every page; the widget is large enough that you would not want it on every page. The facade pattern lets both be true.
bubuu-loader.js
~2 KB over the wire
bubuu-chat.js
~210 KB over the wire
Loader registers the element
bubuu-loader.js defines <bubuu-chat> as a 2 KB facade and renders a chat bubble inside a Shadow DOM. The full bundle starts preloading immediately.
Bubble paints on first frame
No framework boot, no hydration cost. The bubble is a single button with inline styles, so it shows up even on cold cache and slow connections.
Visitor opens the chat
On hover, the preloaded widget bundle is already cached. On click, the cached bundle is promoted from background to foreground.
Widget takes over in place
The full widget upgrades the same DOM node, inheriting every attribute you set on it. The conversation opens immediately — visitors never see a second click.
Every option is a plain HTML attribute on <bubuu-chat>. Set them once in the markup or change them at runtime with element.setAttribute(…). Attributes flagged reactive re-render the widget without losing chat state.
Identifies your account on the Bubuu backend. Every chat and lead created by this instance is attributed to this partner.
Resumes an existing conversation by its server-issued ID. When omitted, a new chat is created on the first user message. Useful for deep-linking back into a previous session from email or a CRM.
UI language. Changing this at runtime swaps the locale without clearing messages — the conversation continues in the new language.
Color scheme. auto follows the host page's prefers-color-scheme and reacts to system theme changes. The widget runs inside a Shadow DOM, so this never affects host styles.
ISO country or region code used to pick locale-specific copy, phone formats, and address parsers. Defaults to the partner's configured region when absent.
Override the agent avatar shown next to assistant messages. Falls back to the partner's brand avatar when omitted.
Display-form phone number shown in the chat header (e.g. +1 (830) 532-5995). Must contain only dialable characters — the widget derives its tel: link by stripping everything else. Falls back to the partner registry, then no phone line at all, when omitted.
Call-to-action label shown on the launcher. Its presence turns the round chat bubble into a wider pill (icon + label). Read once when the widget mounts.
Icon inside the launcher. Pass an emoji or glyph (e.g. 💬) to render it as text, or an image URL (http(s)://, a data:image/… URI, or a root-relative /… path) to render an <img>. Falls back to the default chat icon when omitted.
Which bottom corner the widget anchors to. Case-insensitive; an unrecognised value falls back to bottom-right. For finer control — exact offsets, your own class, media queries — see the Styling API.
Opt in to the widget's session events (conversation started, lead submitted, welcome screen, language/region). Presence enables them; page-events="false" explicitly disables. Off by default, so the widget never dispatches into a page that did not ask for it. The two launcher events are always emitted and need no opt-in.
Page, visitor, and metadata context for the AI as a JSON object — { page, visitor, metadata }. Sent with every message so answers stay grounded in what the visitor is viewing. Malformed JSON is ignored with a console warning; the widget still loads. For objects from JS or a framework, prefer the context property or setContext() — see Context.
Tell the AI what the visitor is looking at. Attach page, visitor, free-form metadata, and the page content itself, and the widget sends it on every message, so the assistant grounds its answers in the exact listing on screen and can pre-fill lead capture. Context rides the existing chat channel — no extra requests, and it survives the facade→full-widget upgrade.
What the visitor is viewing. Auto-derived from the host page (location.href, document.title, document.referrer) when you omit it; anything you pass wins per field.
Known visitor identity. Used to pre-fill the lead form. user is accepted as a friendly alias for visitor.
Free-form key/value bag. Put listing identifiers here — listingId, an OpenImmo or RESO ref, price, location — and the backend resolves them into the matched property. Values are coerced to strings; the backend sanitizes and size-limits them.
The page content the AI should read, handed straight to the backend so it can skip scraping the page. Pass a string (treated as the body) or { body, type }, where type is a format hint (e.g. markdown vs HTML). Omit it to let the backend fetch the page itself.
Use the context attribute for static, server-rendered markup; the context property to bind an object (it replaces the current context); and setContext(patch) to merge a partial update at runtime. getContext() returns the current value. All three reach the same place. Malformed JSON in the attribute is ignored with a console warning — the widget still loads.
Frameworks bind objects to the element's context property. React 19, Vue, Angular, Svelte, and Preact all do this for you. React 18 and below stringify object props, so reach for a ref and call setContext() instead.
Timing is automatic: context set before the chat opens lands on the first message, and an update pushed mid-session applies to the next one. setContext merges, so when the page changes (e.g. SPA navigation) send the new pageContent in full — a partial patch keeps the previous page's body. Updating context never clears the thread or drops the connection — it behaves like the reactive lang and theme attributes. The widget only reads window and document; it adds nothing to the host's global scope.
The widget paints inside a Shadow DOM — host styles can't bleed in, widget styles can't bleed out. Click a mode to retheme the live widget; the switch lands in place, no reload.
Restyle the widget with CSS custom properties on <bubuu-chat> — set them in a stylesheet or inline; they inherit through the widget's shadow DOM. A token you don't set keeps the Bubuu default. A token you do set always wins, even over a partner theme.
Accent color used across the widget: launcher, buttons, links, focus rings.
Text/icon color drawn on top of the accent.
Panel background.
Body text color.
Secondary text color.
Border and divider color.
Corner rounding for messages and controls.
Typeface for the whole widget.
Launcher bubble background.
Launcher icon/text color.
The widget sits 24px from the bottom-right corner by default. Use the position attribute to flip corners, or these tokens to nudge it. The launcher and the chat panel always move together. <bubuu-chat> is itself the fixed-position anchor in your page, so you can also just style it — your own class names and media queries work, and no !important is needed.
Distance from the side edge the widget is anchored to.
Distance from the bottom edge.
Height of anything you pin to the bottom of the page (sticky ad slot, mobile nav). Lifts the full-screen mobile chat sheet clear of it so your bar stays visible. No effect on larger screens, where the panel already follows --bubuu-offset-y.
Stacking order of the whole widget. Raise it if your own overlays cover the launcher.
On phones the chat panel opens full-screen, whatever the position — only the launcher follows the corner. If your page pins something to the bottom on mobile (an ad slot, a nav bar), it will paint over the panel's footer and its main button. Either raise --bubuu-z-index so the chat covers your bar while open, or set --bubuu-panel-inset-bottom to your bar's height so the sheet stops on top of it and your bar stays visible.
Mount <bubuu-chat> as a direct child of <body>: transform, filter, will-change or contain on an ancestor re-bases fixed positioning onto that ancestor and will strand the widget mid-page.
Configure the widget visually, preview the launcher, welcome, conversation and lead form screens together, and copy the embed code.
Open the builder →Pick a locale to flip the live widget. UI strings re-render in place — the chat thread, the open WebSocket, and scroll position survive the switch.
The widget reports what the visitor does as native CustomEvents dispatched on the <bubuu-chat> element. They bubble, so listening on document is enough. Every payload carries a timestamp (epoch ms) and nothing else beyond what is listed below — no names, emails, or message text — so you can pipe them straight into your own analytics.
The two signals from the closed launcher, before any conversation exists. No opt-in needed, and each fires at most once per page load.
The pointer entered the closed launcher for the first time. An early intent signal — useful for measuring how many visitors notice the bubble versus actually open it.
The visitor clicked the launcher and the chat is opening. Closing and reopening the chat later does not repeat it.
Everything that happens inside the chat is opt-in: add the page-events attribute and these start flowing. Without it the widget never dispatches anything into a page that did not ask for it.
The visitor sent their first message. One event per conversation, not per session — resuming an earlier conversation with chat-id does not re-fire it.
The AI-disclosure welcome screen was shown. It appears on every open until the visitor accepts it, so treat this like a pageview rather than a one-shot.
The visitor accepted the welcome screen. Fires once per visitor — returning visitors skip the screen, so this does not repeat on later visits.
The lead form was submitted successfully — the conversion moment. callPreference is how the visitor wants to be contacted: IMMEDIATE, SCHEDULE_LATER, or SCHEDULED. The contact details they typed are not included.
The chat panel opened. Unlike bubuu:open, which fires once on the click that loads the full widget, this is one half of a pair that repeats on every toggle — use it with bubuu:panel-closed to track whether the chat is on screen right now.
The chat panel closed. The conversation is kept, so a later open resumes it rather than starting over.
The widget's UI language changed — either the assistant switched it mid-conversation (it follows the language the visitor writes in) or you set the lang attribute. Only real changes emit: re-applying the active locale is a no-op, so mirroring this event back onto the element cannot loop. Clearing the attribute resets to English silently, without an event.
The active region changed, as an upper-cased ISO country code. Region has no visible UI in the chat, so this is your only signal that it moved. Same no-op and reset rules as bubuu:language-changed.
A single document listener catches the session events, even when you nest <bubuu-chat> inside your own shadow root. The two launcher events do not cross a shadow boundary — if the widget lives inside one, listen on the element itself. Listeners are the entire surface: the widget adds nothing to your page's global scope.
A few patterns that cover most production embeds. Each one stands on its own — copy, swap in your IDs, ship.
<bubuu-chat> is a standard Custom Element. Load the loader once, then render the tag anywhere — React, Vue, Svelte, Angular, or plain HTML all treat it as a native element.
Drop the visitor straight back into their last conversation. Pass chat-id when linking from email follow-ups or saved-search alerts and the widget restores the thread.
On a listing page, push the property the visitor is viewing. The assistant answers about that listing and the lead is saved with it. Re-run setContext whenever the listing changes — the thread and connection stay put.