@leadbot/components
v2.26.0
Published
LeadBot Components: mount agent chat, forms, callback/email requests and WhatsApp handoff into your own DOM.
Readme
@leadbot/components
LeadBot Components building blocks: mount an AI agent chat, lead forms, callback/email requests or a WhatsApp handoff into your own DOM, styled by your site. Rendered into the light DOM (no shadow DOM, no iframe); Preact is bundled internally and invisible to consumers.
Usage
import {
init,
mountAgentChat,
mountForm,
mountCallbackRequest,
mountEmailRequest,
mountWhatsApp,
mountButtons,
mountFlow,
openWidget,
} from "@leadbot/components";
import "@leadbot/components/styles.css";
init({ domainKey: "your-domain-key" });
const chat = mountAgentChat(document.querySelector("#chat")!, {
leadbotId: 18,
id: "chat-step-uuid",
initialMessage: "Hi! How can I help?",
conversationStarters: ["What are your opening hours?"],
});
const form = mountCallbackRequest(document.querySelector("#callback")!, {
leadbotId: 19,
id: "callback-step-uuid",
eventLabel: "Homepage callback",
marksAsQualified: true,
});
// Every mount returns { update(options), destroy() }.
form.update({ submitLabel: "Call me!" });
chat.destroy();
const buttons = mountButtons(document.querySelector("#buttons")!, {
leadbotId: 18,
id: "buttons-step-uuid",
onSelect: (button) => console.log("clicked", button.label),
});
const flow = mountFlow(document.querySelector("#flow")!);Every mount requires a leadbotId — the id of the component leadbot the
component belongs to (each embed can target a different one) — and an id,
the uuid of the published step it renders. Mounts also
accept domainKey / apiHost to override the init() defaults, a
className for the container, and a labels object to override individual UI
strings. Defaults are not English: every component renders its own copy in the
language of the bot as published, which arrives with the boot fetch — the
page's <html lang> is only the fallback until it does.
preview / previewToken exist for the dashboard preview of unpublished draft
flows: preview keeps submissions out of the inbox, previewToken authorizes
the draft. The token is ignored unless preview is set, and it must never be
committed into a production build — anyone reading the shipped JS could then
read your unpublished flows.
Without a bundler (script tag / CDN)
One script tag and one container div — no npm, no build step. The bundle boots
from its own script tag, injects the stylesheet, and mounts every
[data-leadbot-step] container; the component to render comes from the
published step's own type.
<script
src="https://cdn.leadbot.com/components.js"
data-leadbot-domain-key="your-domain-key"
data-leadbot-api-host="https://collector.example.com"
data-leadbot-id="18"
></script>
<div data-leadbot-step="chat-step-uuid"></div>The full API is on window.LeadBot (init, every mount*, mountAll,
openWidget). Container attributes and the rest of the reference live in
docs/SDK.md.
Module formats and subpath entries
The package ships ESM and CommonJS (require works, for older Next.js setups
and CJS toolchains), plus a subpath entry per component:
import { init, mountForm } from "@leadbot/components/form";form, chat, whatsapp, buttons and flow each carry the shared
configuration surface (init, loadFlow, useStep) and nothing from the
other components — a form-only page is ~24 kB gzipped instead of ~81 kB. The
barrel import is tree-shakeable too; the subpath is the guarantee.
Configuration precedence
The value on the mount always wins. Everything in init() is a default;
an option set on a mount*() call overrides it for that mount only, and an
option left undefined overrides nothing. leadbotId included — set it once
in init(), and pass it per mount only where an embed belongs to a different
component leadbot.
Published steps
Every component is backed by a published step. Its id is the step uuid, and
it configures the component from the dashboard — form fields, labels, WhatsApp
number, agent:
mountForm(document.querySelector("#demo")!, { leadbotId: 18, id: "step-uuid" });The flow is fetched once per leadbot per page and shared by every component;
that request is also what counts the pageview. Options you pass explicitly
still win over the published step, but the id itself is not optional: it is
also what the collector validates submissions and chat turns against.
Because the published flow is the single source of truth, unpublishing a bot removes its components from every page — they render nothing until it is published again.
Theming
Everything is restylable, at whichever level suits you:
- Tokens — override a custom property on
.leadbot-componentsor any ancestor (including:root). Defaults are declared on:rootinside@layer leadbot, so an inherited value always wins. - Your own rules — every default in
@leadbot/components/styles.csslives in@layer leadbot, so any unlayered rule of yours beats it regardless of specificity. No!importantneeded:.leadbot-components .leadbot-chatbot-message-bot { background: #111; } - Start from scratch — don't import
styles.cssat all. The markup is plain light DOM with the stable class names listed under Class names; style it however you like.
/* 1. tokens */
.my-section {
--leadbot-primary: #059669;
--leadbot-font-family: "Söhne", system-ui, sans-serif;
--leadbot-radius-card: 4px;
}Dark mode
Set data-leadbot-theme="dark" on <html>, <body> or any ancestor section —
every mount inside it switches to the built-in dark token set (surfaces, text,
borders, feedback colors and the WhatsApp backdrop; layout and the brand greens
are unchanged). data-leadbot-theme="auto" follows the OS
prefers-color-scheme instead. It's all tokens inside @layer leadbot, so your
own overrides keep winning either way.
<html data-leadbot-theme="dark"> <!-- or "auto" -->Tokens
| Property | Default | Purpose |
|---|---|---|
| --leadbot-primary | #2563eb | Buttons, accents, user chat bubbles, focus rings |
| --leadbot-available-height | 600px | Max height of chat / form panels |
| --leadbot-chat-height | 520px | Fixed height of the chat panel — it never grows or shrinks with the conversation. Capped by the available height |
| --leadbot-form-max-width | 360px | Form card width cap; set none for full width |
| --leadbot-form-max-height | available height | Form card height cap; set none to never scroll |
| --leadbot-font-family | page default | Type stack; undeclared so your page's font shows through |
| --leadbot-font-size | 14px | Body text |
| --leadbot-font-size-sm / -xs / -lg | 13px / 12px / 18px | Secondary text, captions, form titles |
| --leadbot-color-text | #1f2937 | Body text |
| --leadbot-color-text-strong | #374151 | Menu items, table headers |
| --leadbot-color-text-body | #4b5563 | Compact labels, quoted text |
| --leadbot-color-text-muted | #6b7280 | Timestamps, citations |
| --leadbot-color-text-subtle | #9ca3af | Placeholders, typing dots, branding |
| --leadbot-color-on-primary | #ffffff | Text/icons on primary and brand fills |
| --leadbot-color-surface | #ffffff | Panels, inputs, menus |
| --leadbot-color-surface-alt | #f9fafb | Table headers, menu hover |
| --leadbot-color-surface-sunken | #f3f4f6 | Bot chat bubbles |
| --leadbot-color-surface-tray / -tray-alt | #f8f9fa / #f0f0f0 | Composer trays |
| --leadbot-color-border | #e5e7eb | Dividers, default borders |
| --leadbot-color-border-input / -field / -checkbox | #ddd / #e0e3e8 / #d1d5db | Field borders |
| --leadbot-color-disabled | #ccc | Disabled send button |
| --leadbot-color-logo | #111827 | "Powered by" wordmark |
| --leadbot-color-error | #ef4444 | Field validation |
| --leadbot-color-error-bg / -fg / -border | #fee / #c33 / #fcc | Form error banner |
| --leadbot-color-success | #059669 | Success message |
| --leadbot-color-success-text | #065f46 | "Opened in WhatsApp" confirmation |
| --leadbot-color-online | #34d399 | Agent online dot |
| --leadbot-focus-ring / -soft | primary @ 15% / 12% | Focus rings on text controls |
| --leadbot-radius-card | 12px | Panel border radius |
| --leadbot-radius-sm / -md / -pill | 6px / 8px / 20px | Inputs / buttons / pills |
| --leadbot-spacing-card | 16px | Panel padding |
| --leadbot-gradient-card | white gradient | Form card background |
| --leadbot-border-light | 1px solid rgba(0,0,0,.12) | Card border |
| --leadbot-shadow-sm / -md / -lg | — | Elevation |
| --leadbot-duration-fast / -normal / -slow | 150ms / 300ms / 500ms | Transitions |
| --leadbot-ease-appear / -interactive / -loop / -bounce | — | Easing curves |
| --leadbot-whatsapp-green / -hover / -on-green | #42d66b / #35c95d / #111b21 | WhatsApp panel chrome |
| --leadbot-whatsapp-wallpaper / -end | #e5ddd5 / #d9d0c4 | WhatsApp chat backdrop |
| --leadbot-whatsapp-bubble / -text | #ffffff / #303030 | WhatsApp message bubble |
| --leadbot-whatsapp-tick | #4fc3f7 | Read receipts |
| --leadbot-whatsapp-brand / -hover / -dark | #25d366 / #1fb959 / #128c7e | Official WhatsApp brand fills |
Alternatively pass baseColor (e.g. baseColor: "#ff0076" in a mount, the
React/Vue provider, or init()) — it sets --leadbot-primary on the mount
container for you.
The spacing scale, circular radii, fixed element sizes (avatars, icons, send buttons) and one-off shadows are deliberately not tokenized — override them with a rule against the class instead (level 2 above).
Class names
The components render the widget's own step markup, so these are the classes
the widget uses too. Stable and semantic; tests/components_styles.test.ts
fails if a component renders a class the stylesheet doesn't style.
- Forms —
leadbot-form·leadbot-callback-form·leadbot-email-request-form·leadbot-form-shell·leadbot-form-scroll·leadbot-form-title·leadbot-form-fields·leadbot-form-actions·leadbot-form-error·leadbot-form-complete·leadbot-success-message·leadbot-submit-btn(+leadbot-loading) ·leadbot-field·leadbot-field-checkbox·leadbot-field-error·leadbot-field-list·leadbot-field-list-label·leadbot-field-options·leadbot-field-option·leadbot-required-asterisk, and the date and time pickers'leadbot-datepicker-*/leadbot-timepicker-* - Agent chat —
leadbot-chatbot-interfaceand its-header,-header-info,-title,-status,-online-dot,-menu-btn,-menu,-menu-item,-menu-item-icon,-messages,-message,-message-bot,-message-user,-message-typing,-message-content,-thinking,-thinking-text,-typing-dots,-citations,-citation-item,-citation-number,-citation-url,-starters,-starter-btn,-input,-input-field,-message-input,-send-btn(+--active) suffixes - WhatsApp —
leadbot-whatsapp-interfaceand its-header,-contact-info,-contact-details,-name,-status,-chat,-message,-message-content,-input-container,-input-bar,-message-field,-send-btnsuffixes, plusleadbot-whatsapp-send-disabled - Buttons —
leadbot-options(alsoleadbot-buttons) holdingleadbot-option-btn(alsoleadbot-buttons-btn;-secondarywithout a step colour,-iconfor tel:/mailto:)
DOM anatomy
The class names alone don't tell you which element carries what, so here is the
tree each component renders. This nesting is public API — it changes only on
a major version, and display: contents on a wrapper marked (structural) is a
supported way to flatten it.
div.leadbot-components mount container (yours; also .leadbot-bare)
└─ div.leadbot-form surface: background, border, shadow, radius,
│ size caps (also .leadbot-callback-form /
│ .leadbot-email-request-form)
└─ form.leadbot-form-shell (structural) flex column
├─ div.leadbot-form-scroll (structural) card padding + scroll region
│ ├─ div.leadbot-form-title
│ ├─ div.leadbot-form-error
│ └─ fieldset.leadbot-form-fields (structural)
│ └─ div.leadbot-field[data-field][data-field-type] × N
│ ├─ label
│ ├─ input | select | textarea ([aria-invalid] on error)
│ └─ p.leadbot-field-error the field's message
└─ div.leadbot-form-actions surface: own background + top border
└─ button.leadbot-submit-btnAfter a successful submit the whole tree is replaced by:
div.leadbot-form.leadbot-form-complete
└─ div.leadbot-success-messageEvery field carries data-field="<name>" and data-field-type="<canonical
type>" — target those instead of :nth-child, which silently re-pairs when
fields are reordered in the dashboard.
Chrome, size and layout
Three opinions in the default styling are widget-isms, and each has an off switch:
chrome: falseon any mount removes the card surface, the size caps, the scroll region and the actions bar, leaving only functional styling (field stacking, focus rings, error and success states). Use it whenever the form is part of your page rather than a floating panel.--leadbot-form-max-width(360px) and--leadbot-form-max-height(--leadbot-available-height) — set either tononeto keep the card but drop the cap.layout: "inline"onmountFormputs the submit button on the same row as the last field; earlier fields span the full width. Pairing is positional.
Recipes
/* Full-width form, no card — pairs with chrome: false */
.leadbot-components .leadbot-field input { border-radius: 999px; padding: 14px 18px; }
.leadbot-components .leadbot-submit-btn { border-radius: 999px; width: auto; padding: 14px 28px; }/* Inline "url + email + button" capture row, 2/3 – 1/3 */
mountForm(el, { leadbotId: 18, id: stepUuid, chrome: false, layout: "inline", hideLabels: true });/* ...and the 2/3 – 1/3 split, by field name rather than by position */
.leadbot-components .leadbot-form-shell { grid-template-columns: 2fr 1fr; }
.leadbot-components [data-field="email"] { grid-column: 1; }<!-- Dark section — full built-in dark theme for the mounts inside it -->
<section data-leadbot-theme="dark">...</section>Components ships no focus-visible, reduced-motion or responsive rules (those are
widget-only partials) — if you replace styles.css wholesale, those states are
yours to provide.
Mounts
mountAgentChat(el, { leadbotId, id, initialMessage?, conversationStarters?, placeholder?, labels?, eventLabel?, chatSessionId?, onMessage?, onError?, className? })mountForm(el, { leadbotId, id, fields?, stepType?, submitLabel?, successMessage?, eventLabel?, marksAsQualified?, onSubmit?, onSuccess?, onError?, labels?, className?, chrome?, layout?, hideLabels? })—chrome: falsedrops the widget card,layout: "inline"puts the button beside the last field,hideLabels: truekeeps labels for screen readers only and uses them as placeholdersmountCallbackRequest(el, options)—mountFormpreset (name + phone,stepType: "callback_request"); the published step or an explicitfieldsarray overrides the presetmountEmailRequest(el, options)— preset (name + email + message,stepType: "email_request")mountWhatsApp(el, { leadbotId, id, phoneNumber?, prefilledMessage?, ... })— records the interaction, then opens WhatsAppmountButtons(el, { leadbotId, id, buttons?, eventLabel?, onSelect?, className? })— renders a published step's options as buttons; an option with anext_step_uuidopens the page widget at that step and sends its label as the first message; an explicitbuttonsarray overrides the published options. A button may setchildren(any markup — icons etc.) to render instead of itslabel; the label still identifies the button for tracking andonSelectmountFlow(el, { leadbotId?, typingIndicator?, ... })— renders a full leadbot flow (a standard bot marked "served via SDK") inline in your page, with no widget chromeopenWidget(params?: { stepUuid?, message? })/closeWidget()/toggleWidget()— control an embedded LeadBot widget on the same page; passing bothstepUuidandmessagenavigates to that step and sends the messageclearFlowCache()— drops the page-lifetime boot-fetch cache, so the next mount (or an SPA route change) refetches the published flow
Full documentation: docs/SDK.md in the repo (published docs mirror it).
Embedded flows
A standard leadbot flow — built in the dashboard and marked Settings → Serve via SDK — renders inline anywhere on your page via mountFlow(). It's the full conversation flow (all steps, routing, state) with no widget chrome: no floating panel, toggle button, or fixed positioning. Your host element decides the size and placement.
mountFlow(document.querySelector("#leadbot-flow")!, {
leadbotId: 18, // falls back to init() default
typingIndicator: true, // show typing dots and navigation delay (default)
});The bot ID can be passed explicitly or defaults to the leadbotId in init(). The boot fetch selects the bot by ID, counts the pageview, and respects the bot's visible URLs setting — if the page falls outside that scope, nothing renders.
Import @leadbot/components/styles.css for the default widget-like look (scoped to .leadbot-flow), or style the .leadbot-* markup yourself. The publication's dashboard base color applies automatically; pass baseColor to override it.
Conversation state restores across page navigations if the bot has keep open on navigation enabled (stored per leadbot in sessionStorage). On SPA route changes, a new pageview is billed and the conversation restarts unless the restore setting keeps it open.
Multiple mountFlow() calls with different leadbotIds can coexist on one page, each maintaining separate conversation state. A call returns { update(options), destroy() } — update() re-renders in place and preserves conversation history, while destroy() cleans up. The classic LeadBot widget (via window.LeadBot or script.js) remains unaffected.
Same renderer as the widget
Forms, chat, WhatsApp and buttons now render the widget's own step components and styles, so a published step looks and behaves the same in both. What that changes for an existing integration:
- Validation — empty required fields and malformed emails get the
browser's own message, as in the widget;
labels.required,invalidEmailandinvalidNumberare deprecated. Phone numbers are cleaned up before they are checked and sent (0031 6 …→+316…), and server errors land on their field in the bot's language. - Data — checklists reach the collector and
onSubmit/onSuccessas arrays (they were a JSON string); checkboxes stay booleans. Field types the widget doesn't know are skipped instead of shown as a text box. Dates and times use the widget's pickers. - Chat — shows the step's sender name and avatar, a "powered by" line (hidden for white-label bots), feedback buttons, and the forms and WhatsApp hand-offs the agent opens mid-conversation. The conversation survives a page load within the tab.
- WhatsApp — the QR code on desktop, the phone-number question and the page
link in the message all follow the step's settings.
onSuccessfires once the message is saved. - Buttons — option conditions (device, day, time) apply; an option that leads to a button step shows that button, so tel:, mailto: and links open directly; each click is tracked against the button's own step.
- Flow — the dashboard preview's submissions are flagged as preview, and new steps scroll into view.
Upgrading to 0.5
0.5 makes the published step the source of truth, which is a breaking change for every component:
idis now required on every mount and component. It is the uuid of the published step, from the dashboard. Without it a component renders nothing.agentIdis gone frommountAgentChat— pass the chat step'sidinstead and the collector resolves the agent from it.phoneNumberis now optional onmountWhatsApp; omit it to use the published step's number.init({ leadbotId })is new. Without it there is no boot fetch, so no pageview is counted and no GA events fire on a first-ever visit. Pass it to get the same tracking as the classic widget.
- mountAgentChat(el, { leadbotId: 18, agentId: "agent-uuid" });
+ mountAgentChat(el, { leadbotId: 18, id: "chat-step-uuid" });