#React Widget
Drop-in chat widget and hooks for React 18/19. Built on @bootdesk/js-web-adapter-core.
#Installation
npm install @bootdesk/js-web-adapter-react @bootdesk/js-web-adapter-core
Peer deps: react ^18 || ^19, react-dom, marked ^18, dompurify ^3.4.
Import the CSS:
import "@bootdesk/js-web-adapter-react/styles.css";
#ChatWidget
The main component. Three display modes: floating, fullscreen, and embedded.
#Floating Mode (default)
import { ChatWidget } from "@bootdesk/js-web-adapter-react";
import { WebChatClient } from "@bootdesk/js-web-adapter-core";
const client = new WebChatClient({
apiUrl: "https://your-app.com",
userId: "user-123",
userName: "Alice",
});
function App() {
return (
<ChatWidget
client={client}
title="Support"
placeholder="Type your question..."
position="bottom-right"
theme="auto"
/>
);
}
#Fullscreen Mode
<ChatWidget client={client} initialMode="fullscreen" />
#Embedded Mode
Takes the full size of its parent container. Use when you want to render the chat inline on a page or inside an iframe.
<div style={{ height: "600px" }}>
<ChatWidget client={client} embedded />
</div>
Auto-detects iframe — when inside an <iframe>, switches to embedded mode automatically and listens for chat-config messages from the parent page via useIframeBridge.
#Props
| Prop | Type | Default |
|---|---|---|
client |
WebChatClient |
required |
locale |
string | { locale: string, overrides?: PartialLocaleStrings } |
"en" |
initialMode |
"floating" | "fullscreen" |
"floating" |
theme |
"light" | "dark" | "auto" |
"auto" |
title |
string |
"Chat" |
placeholder |
string |
"Type a message..." |
position |
"bottom-right" | "bottom-left" | "top-right" | "top-left" |
"bottom-right" |
embedded |
boolean |
false |
showClose |
boolean |
true |
showFullscreenToggle |
boolean |
true |
enableAttachments |
boolean |
false |
uploadConfig |
UploadConfig? |
— |
accept |
string? |
— |
maxFileSize |
number? |
— |
onOpen |
() => void? |
— |
onClose |
() => void? |
— |
onThemeChange |
(theme) => void? |
— |
floatingButton.icon |
ReactNode? |
chat bubble SVG |
floatingButton.badgeCount |
number? |
— |
floatingButton.size |
number |
56 |
floatingButton.backgroundColor |
string? |
var(--chat-primary) |
className |
{ container?, header?, messageList?, inputArea? }? |
— |
preEntry |
{ render: (helpers: { start, t, locale }) => ReactNode }? |
— |
onChatStart |
(config?: ReconfigureConfig) => void? |
— |
#Pre-Entry Screen
Show a custom form before the conversation starts — useful for collecting a name, email, verification code, or terms acceptance. The developer controls all logic; call start(config) when ready.
The config passed to start() is forwarded to client.reconfigure() (see JS Core → Reconfiguration), updating the client's identity before messages load.
The render function also receives t(path) for translating strings and locale for the current locale code — useful when your pre-entry form needs localized text:
<ChatWidget
locale="pt-BR"
preEntry={{
render: ({ start, t, locale }) => (
<form>
<h1>{t("chatWidget.title")}</h1>
<button onClick={() => start()}>{t("common.start")}</button>
</form>
),
}}
/>
function EmailVerificationForm({ start }) {
const [email, setEmail] = useState("");
const [code, setCode] = useState("");
const [step, setStep] = useState("email");
const [error, setError] = useState("");
const handleEmailSubmit = async (e) => {
e.preventDefault();
// Send code to user's email
const { id } = await fetch("/api/request-code", {
method: "POST",
body: JSON.stringify({ email }),
}).then((r) => r.json());
setStep("code");
};
const handleCodeSubmit = async (e) => {
e.preventDefault();
const data = await fetch("/api/verify-code", {
method: "POST",
body: JSON.stringify({ code }),
}).then((r) => r.json());
// Configure the client and start the conversation
start({ userId: data.userId, verifyToken: data.verifyToken });
};
if (step === "email") {
return (
<form onSubmit={handleEmailSubmit}>
<h2>Welcome!</h2>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com"
required
/>
<button type="submit">Send Code</button>
{error && <p style={{ color: "red" }}>{error}</p>}
</form>
);
}
return (
<form onSubmit={handleCodeSubmit}>
<h2>Enter the 6-digit code</h2>
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="000000"
maxLength={6}
required
/>
<button type="submit">Verify</button>
</form>
);
}
function App() {
return (
<ChatWidget
client={client}
title="Support"
preEntry={{
render: ({ start, t }) => (
<EmailVerificationForm start={start} title={t("chatWidget.title")} />
),
}}
onChatStart={(config) => {
// Persist session so returning users skip the form
document.cookie = `session=${JSON.stringify(config)}; max-age=604800`;
}}
/>
);
}
Messages only begin loading after start() is called. While the pre-entry form is shown, the header remains visible (close button, theme toggle, etc.). Works in all three display modes (floating, fullscreen, embedded).
#Hooks
#useChatClient
Creates a WebChatClient, calls connect() on mount and disconnect() on unmount.
import { useChatClient } from "@bootdesk/js-web-adapter-react";
function MyComponent() {
const client = useChatClient({
apiUrl: "https://your-app.com",
userId: "user-123",
userName: "Alice",
});
return <div>{/* ... */}</div>;
}
#useMessages
Full message state management.
import { useChatClient, useMessages } from "@bootdesk/js-web-adapter-react";
function Chat() {
const client = useChatClient({ apiUrl, userId, userName });
const {
messages,
loading,
hasMore,
loadMore,
sendMessage,
editMessage,
deleteMessage,
addReaction,
removeReaction,
canEdit,
canDelete,
canReact,
} = useMessages(client);
return (
<div>
<button onClick={() => sendMessage("Hi!")} disabled={loading}>
Send
</button>
<button onClick={() => addReaction("msg-1", "👍")}>Like</button>
{messages.map((msg) => (
<div key={msg.id}>{msg.content.text}</div>
))}
{hasMore && <button onClick={loadMore}>Load more</button>}
</div>
);
}
#useStreaming
import { useStreaming } from "@bootdesk/js-web-adapter-react";
function StreamView({ client }) {
const { streamingMessages, isStreaming } = useStreaming(client);
return <div>{/* streamingMessages: Map<string, string> */}</div>;
}
#useTyping
import { useTyping } from "@bootdesk/js-web-adapter-react";
function Typing({ client }) {
const { isSomeoneTyping } = useTyping(client);
return isSomeoneTyping ? <span>Typing...</span> : null;
}
#useAttachmentUpload
const { pendingAttachments, upload, removePending, progress } =
useAttachmentUpload(uploadConfig);
#Cards
Cards are rich interactive elements sent from your backend. The widget renders them automatically.
#Web Card Types
Six built-in web-native card types are available. Sent from your backend, rendered automatically:
| Type | PHP Class | Description |
|---|---|---|
video |
WebVideoCard |
Video embed (YouTube/Vimeo auto-detect, generic fallback) |
audio |
WebAudioCard |
Audio player |
location |
WebLocationCard |
Map with OpenStreetMap static tile |
product |
WebProductCard |
Product with image, price, badge, action buttons |
poll |
WebPollCard |
Interactive poll with results bar |
carousel |
WebCarouselCard |
Horizontal scroll of card items |
PHP usage (via Thread::post() or Chat::reply()):
use BootDesk\ChatSDK\Web\Cards\WebVideoCard;
use BootDesk\ChatSDK\Web\Cards\WebPollCard;
use BootDesk\ChatSDK\Core\PostableMessage;
// Video card
$chat->reply(PostableMessage::card(
new WebVideoCard(
url: 'https://youtube.com/watch?v=abc123',
title: 'Tutorial',
thumbnail: 'https://img.youtube.com/vi/abc123/maxresdefault.jpg',
duration: 300,
platform: 'youtube', // 'youtube', 'vimeo', or null for generic <video>
),
));
// Poll card
$poll = new WebPollCard(
question: 'Which framework?',
options: [
['id' => 'laravel', 'label' => 'Laravel'],
['id' => 'symfony', 'label' => 'Symfony'],
],
allowMultiple: false,
results: [ // optional — show bars immediately
['optionId' => 'laravel', 'count' => 42],
['optionId' => 'symfony', 'count' => 18],
],
);
$chat->reply(PostableMessage::card($poll));
// Carousel (items are regular Cards)
use BootDesk\ChatSDK\Core\Cards\Card;
use BootDesk\ChatSDK\Core\Cards\Button;
use BootDesk\ChatSDK\Web\Cards\WebCarouselCard;
$carousel = new WebCarouselCard(items: [
Card::make()
->header('Product A')
->text('$29.99')
->actions([Button::primary('Buy', 'buy_a')]),
Card::make()
->header('Product B')
->text('$49.99')
->actions([Button::primary('Buy', 'buy_b')]),
]);
$chat->reply(PostableMessage::card($carousel));
Poll votes and product actions trigger ActionEvent. Listen in your bot:
use BootDesk\ChatSDK\Core\ActionEvent;
$chat->listen(ActionEvent::class, function (ActionEvent $event) {
match ($event->actionId) {
'poll_vote' => /* handle vote for $event->value */,
'buy' => /* process purchase for $event->value */,
default => null,
};
});
#Custom Card Renderers
Register custom renderers via CardProvider:
import {
CardProvider,
CardRenderer,
DefaultCard,
} from "@bootdesk/js-web-adapter-react";
function App() {
return (
<CardProvider
renderers={{
"my-custom-card": MyCustomCard,
default: DefaultCard,
}}
>
<ChatWidget client={client} />
</CardProvider>
);
}
function MyCustomCard({ card }) {
return <div className="my-card">{card.data.text}</div>;
}
ChatProvider wraps CardProvider automatically if you prefer a single provider:
import { ChatProvider, ChatWidget } from "@bootdesk/js-web-adapter-react";
<ChatProvider>
<ChatWidget client={client} />
</ChatProvider>;
#Internationalisation
33 built-in locales including en, en-US, en-GB, pt, pt-BR, pt-PT, es, fr, de, it, nl, ja, zh-CN, zh-TW, ko, ar, and more.
Set locale on ChatWidget directly:
<ChatWidget client={client} locale="pt-BR" />
Pass runtime overrides via LocaleConfig:
<ChatWidget
client={client}
locale={{
locale: "en",
overrides: { chatWidget: { title: "Support" } },
}}
/>
Register a completely custom locale with registerLocale:
import { registerLocale } from "@bootdesk/js-web-adapter-react";
registerLocale("my-custom", {
direction: "ltr",
chatWidget: {
title: "Suporte",
placeholder: "Digite sua mensagem...",
// ... all required fields
},
// ...
});
<ChatWidget client={client} locale="my-custom" />;
registerLocale uses the same LocaleStrings type — your IDE will autocomplete required fields.
Access translations inside components with useLocale():
import { useLocale } from "@bootdesk/js-web-adapter-react";
function MyComponent() {
const { t, locale, dir, strings } = useLocale();
return <div dir={dir}>{t("chatWidget.title")}</div>;
}
#Styling
The widget uses a CSS component class system with the bdesk-* (bootdesk-chat) prefix. Every visual element has a dedicated class, making it easy to target and override with your own CSS.
#CSS Component Classes
All classes are defined in the widget's CSS via Tailwind @apply. They resolve to the CSS custom properties listed below, so changing a variable updates all components automatically.
| Area | Class | Element |
|---|---|---|
| Widget | bdesk-widget |
Root container (embedded) |
bdesk-widget--float |
Floating mode wrapper | |
bdesk-widget--fullscreen |
Fullscreen mode overlay | |
bdesk-widget--pos-{bottom-right,bottom-left,top-right,top-left} |
Floating position | |
bdesk-widget--float-size |
Floating dimensions + shadow | |
bdesk-pre-entry |
Pre-entry screen wrapper | |
| Header | bdesk-header |
Header bar |
bdesk-header-left |
Left section (dot + title) | |
bdesk-header-dot |
Connection status dot | |
bdesk-header-dot--connected |
Connected state | |
bdesk-header-dot--disconnected |
Disconnected state | |
bdesk-header-title |
Title text | |
bdesk-header-right |
Right button group | |
bdesk-header-btn |
Any header button | |
| Message List | bdesk-message-list |
Scrollable container |
bdesk-message-group |
Consecutive messages from same author | |
bdesk-message-group-author |
Author name above group | |
bdesk-message-item |
Single message wrapper | |
bdesk-message-bubble-own |
Current user's bubble | |
bdesk-message-bubble-other |
Other user's bubble | |
bdesk-message-text |
Message text content | |
bdesk-msg-timestamp |
Timestamp below message | |
bdesk-reactions |
Reaction row | |
bdesk-reaction-btn |
Single reaction button | |
bdesk-reaction-btn--active |
User has reacted | |
bdesk-reaction-btn--inactive |
User has not reacted | |
bdesk-reaction-count |
Reaction count | |
bdesk-empty-state |
Empty message list | |
bdesk-empty-state-text |
Empty state text | |
bdesk-loading |
Loading skeleton wrapper | |
bdesk-loading-dots |
Loading dot container | |
bdesk-loading-dot |
Individual bounce dot | |
bdesk-thinking |
Thinking indicator wrapper | |
bdesk-thinking-dots |
Thinking dot container | |
bdesk-thinking-dot |
Individual bounce dot | |
bdesk-scroll-anchor |
Invisible scroll target | |
| Input Area | bdesk-input-area |
Input area container |
bdesk-input-area-row |
Input + send button row | |
bdesk-input-area-attach |
Attachment toggle button | |
bdesk-input-area-attach--active |
Dropzone visible state | |
bdesk-input |
Message textarea | |
bdesk-send-btn |
Send button | |
bdesk-spinner |
Uploading spinner | |
| Typing | bdesk-typing-indicator |
Typing indicator bar |
bdesk-typing-wrapper |
Dot + label wrapper | |
bdesk-typing-dots |
Dot container | |
bdesk-typing-dot |
Individual bounce dot | |
| Floating Button | bdesk-floating-btn |
FAB button |
bdesk-floating-btn-badge |
Unread count badge | |
| Dropzone | bdesk-dropzone |
File drop zone |
bdesk-dropzone--dragging |
Drag active state | |
bdesk-dropzone--disabled |
Disabled state | |
bdesk-dropzone-input |
Hidden file input | |
bdesk-dropzone-center |
Centered content wrapper | |
bdesk-dropzone-icon |
Upload icon | |
bdesk-dropzone-text |
Instruction text | |
| Attachments | bdesk-attachment-list |
Attachment list |
bdesk-attachment-item |
Single attachment | |
bdesk-attachment-item--error |
Upload failed | |
bdesk-attachment-name |
File name | |
bdesk-attachment-name--error |
Failed file name | |
bdesk-attachment-size |
File size / status | |
bdesk-attachment-progress |
Progress bar track | |
bdesk-attachment-progress-fill |
Progress bar fill | |
bdesk-attachment-remove |
Remove button | |
| Push Notifications | bdesk-push-prompt |
Permission prompt |
bdesk-push-prompt-body |
Text container | |
bdesk-push-prompt-title |
Prompt title | |
bdesk-push-prompt-desc |
Prompt description | |
bdesk-push-prompt-actions |
Button row | |
bdesk-push-prompt-enable |
Enable button | |
bdesk-push-prompt-disable |
Disable button | |
bdesk-push-prompt-dismiss |
Dismiss button | |
bdesk-push-toggle |
Toggle label | |
bdesk-push-toggle-input |
Checkbox | |
bdesk-push-toggle-text |
Label text | |
bdesk-push-unsupported |
Unsupported message | |
bdesk-push-denied |
Denied message | |
| Cards | bdesk-card |
Card container |
bdesk-card-header |
Card header | |
bdesk-card-img |
Card image | |
bdesk-card-section |
Card section | |
bdesk-card-section-text |
Section text | |
bdesk-card-field |
Key-value field | |
bdesk-card-field-title |
Field label | |
bdesk-card-field-value |
Field value | |
bdesk-card-element-text |
Text element | |
bdesk-card-element-text--muted |
Muted style | |
bdesk-card-element-text--bold |
Bold style | |
bdesk-card-divider |
Horizontal rule | |
bdesk-card-link |
Hyperlink | |
bdesk-card-table |
Table | |
bdesk-card-table-th |
Table header cell | |
bdesk-card-table-td |
Table data cell | |
bdesk-card-link-btn |
Link-style button | |
bdesk-card-link-btn--primary |
Primary style | |
bdesk-card-link-btn--danger |
Danger style | |
bdesk-card-link-btn--default |
Default style | |
bdesk-card-img-element |
Image element | |
bdesk-card-actions |
Action button row | |
bdesk-card-action-btn |
Action button | |
bdesk-card-action-btn--primary |
Primary action | |
bdesk-card-action-btn--danger |
Danger action | |
bdesk-card-action-btn--default |
Default action | |
bdesk-image-card |
Image card wrapper | |
bdesk-image-card-img |
Image card image | |
bdesk-image-card-title |
Image card caption | |
bdesk-file-card |
File card wrapper | |
bdesk-file-card-icon |
File type icon | |
bdesk-file-card-info |
File metadata | |
bdesk-file-card-name |
File name | |
bdesk-file-card-size |
File size | |
bdesk-file-card-download |
Download link | |
bdesk-video-card |
Video card wrapper | |
bdesk-video-card-embed |
Video embed container | |
bdesk-video-card-iframe |
Embed iframe | |
bdesk-video-card-player |
Native <video> element |
|
bdesk-video-card-title |
Video title | |
bdesk-video-card-duration |
Duration text | |
bdesk-audio-card |
Audio card wrapper | |
bdesk-audio-card-title |
Audio title | |
bdesk-audio-card-player |
Native <audio> element |
|
bdesk-audio-card-duration |
Duration text | |
bdesk-location-card |
Location card wrapper | |
bdesk-location-card-map |
Map image link | |
bdesk-location-card-img |
Map tile image | |
bdesk-location-card-info |
Info container | |
bdesk-location-card-title |
Location title | |
bdesk-location-card-address |
Address text | |
bdesk-location-card-directions |
"Open in Maps" link | |
bdesk-product-card |
Product card wrapper | |
bdesk-product-card-img |
Product image | |
bdesk-product-card-body |
Product info body | |
bdesk-product-card-header |
Title + price row | |
bdesk-product-card-title |
Product name | |
bdesk-product-card-price |
Price text | |
bdesk-product-card-badge |
Badge label | |
bdesk-product-card-actions |
Action button row | |
bdesk-product-card-btn |
Action button | |
bdesk-poll-card |
Poll card wrapper | |
bdesk-poll-card-question |
Question text | |
bdesk-poll-card-options |
Options container | |
bdesk-poll-card-option |
Single option button | |
bdesk-poll-card-option--selected |
Selected option | |
bdesk-poll-card-option--voted |
Voted option | |
bdesk-poll-card-option-label |
Option label | |
bdesk-poll-card-option-bar-wrap |
Progress bar wrapper | |
bdesk-poll-card-option-bar |
Progress bar fill | |
bdesk-poll-card-option-pct |
Percentage text | |
bdesk-poll-card-vote-btn |
Vote submit button | |
bdesk-poll-card-total |
Total votes count | |
bdesk-carousel-card |
Carousel card wrapper | |
bdesk-carousel-card-track |
Horizontal scroll track | |
bdesk-carousel-card-item |
Individual slide | |
| Message Attachments | bdesk-img-attach |
Inline image |
bdesk-file-attach |
Inline file link | |
bdesk-file-icon |
File link icon | |
bdesk-attach-mt |
Attachment top margin | |
| Error | bdesk-error-boundary |
Error fallback |
bdesk-error-boundary-title |
Error heading | |
bdesk-error-boundary-msg |
Error message | |
bdesk-error-boundary-retry |
Retry button |
#Overriding Styles
Target any bdesk-* class in your own CSS. Because the widget does not apply a global CSS reset, your page's styles remain unaffected.
/* Custom header background */
.bdesk-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
/* Rounded own bubbles */
.bdesk-message-bubble-own {
border-radius: 12px 12px 2px 12px;
}
/* Larger send button */
.bdesk-send-btn {
padding: 0.75rem 1.5rem;
font-size: 1rem;
}
#className Prop
Components accept a className prop for per-instance overrides. The widget uses cn() (powered by clsx + tailwind-merge) so your classes merge cleanly without conflicts:
<ChatWidget
client={client}
className={{
header: "bg-gradient-to-r",
messageList: "px-2",
inputArea: "border-t-2",
}}
/>
<MessageList
messages={messages}
className="bg-gray-50"
/>
#CSS Variables
Override on your root element for a consistent theme:
:root {
--chat-primary: #4f46e5;
--chat-primary-hover: #4338ca;
--chat-background: #ffffff;
--chat-surface: #f8fafc;
--chat-text: #0f172a;
--chat-text-secondary: #64748b;
--chat-border: #e2e8f0;
--chat-own-message: #4f46e5;
--chat-own-message-text: #ffffff;
--chat-other-message: #f1f5f9;
--chat-other-message-text: #0f172a;
--chat-error: #ef4444;
--chat-success: #22c55e;
--chat-font-family: "Inter", system-ui, sans-serif;
}
#Theme Toggle
The header includes a theme toggle button by default. Listen for changes with onThemeChange. Theme is persisted to localStorage key chat-theme.
#Responsive
Screens narrower than 800px automatically switch to fullscreen. Floating position and drawer are optimised for mobile — dvh units keep the input visible on iOS, and interactive-widget=resizes-content viewport meta is managed for Android.
#Sub-components
You can also use individual components for a custom layout:
import {
Header,
MessageList,
InputArea,
TypingIndicator,
} from "@bootdesk/js-web-adapter-react";
function CustomChat({ client }) {
const { messages, sendMessage } = useMessages(client);
const { isSomeoneTyping } = useTyping(client);
return (
<div className="my-chat">
<Header title="Support" />
<MessageList messages={messages} />
{isSomeoneTyping && <TypingIndicator />}
<InputArea onSend={sendMessage} />
</div>
);
}