@eworx-org/ccbot-react
@eworx-org/ccbot-react
A React widget library for the Content Cloud chatbot. Renders a floating, conversational AI chat interface that connects to the Content Cloud chat service.
Contents
- Installation
- Quick Start
- Props Reference
- Full Example
- Implementing a Custom Token Endpoint (Non-Drupal Sites)
- Styling
Installation
npm install @eworx-org/ccbot-react
# or
yarn add @eworx-org/ccbot-react
# or
pnpm add @eworx-org/ccbot-react
Peer dependencies
React 18 or 19 is required and must be installed in your project:
npm install react react-dom
Quick Start (Minimum Required Props)
The component only requires auth to function.
import { CCbotWidget } from '@eworx-org/ccbot-react';
export default function App() {
return (
<CCbotWidget
auth={{ tokenEndpoint: '/ccbot/api/token' }}
/>
);
}
Note:
configis optional. Provide it only when you want to override widget defaults. The widget mounts inside a Shadow DOM and ships its own styles — there is no separate stylesheet to import.
Props Reference
The component accepts a single CcbotWidgetProps object with the following top-level fields.
apiBaseUrl — string · Optional
Base URL of the Content Cloud chat service the widget talks to. This is integration wiring, so it is a top-level prop (not part of config) and is never sourced from remote config. Use it to point at an internal staging/test service; omit it for the default production URL baked into the package.
storagePrefix — string · Optional · Default 'ccbot'
localStorage namespace for this widget's persisted state. Every key derives from it with a static suffix: <prefix>:messages (conversation), <prefix>:open (open/closed state), <prefix>:config (cached remote config), and <prefix>:anonymous-user-id (stable anonymous identity). Like apiBaseUrl, it is integration wiring: a top-level prop, never sourced from remote config. Set a distinct value if you mount more than one widget on the same origin (e.g. different tenants), so their stored state and cached config don't collide.
config — CcbotWidgetConfig · Optional
General configuration for the chat widget's appearance and behaviour. All of these may also be supplied via remote configuration.
| Prop | Type | Default | Description |
|---|---|---|---|
threadWelcomeAvatarLabel |
string |
'CCBot' |
Deprecated. Use headerTitle instead. |
threadWelcomeMessage |
ReactNode | ComponentType |
— | Deprecated. Use welcomeMessage instead. |
suggestedQuestionsAllContent |
string[] |
[] |
A list of suggested questions shown when the user is querying across all content. |
suggestedQuestionsThisPage |
string[] |
[] |
A list of suggested questions shown when the user is querying within the current page scope. |
downloadFilename |
string | (context) => string |
'conversation-YYYY-MM-DD_HH-mm.txt' |
Controls the filename used when users download a conversation. Pass a string for a fixed filename or a function for full control. .txt is appended automatically if omitted. |
pageScope |
CcbotPageScopeConfig |
— | Optional configuration for page-level context. See pageScope below. |
defaultOpen |
boolean |
false |
When true, the widget is rendered open on the first visit. See defaultOpen & openBehavior below. |
openBehavior |
'greet-once' | 'remember' | 'always' |
'greet-once' |
Controls whether the widget auto-opens on subsequent visits. Only takes effect when defaultOpen is true. See defaultOpen & openBehavior below. |
theme |
'black' | 'white' | 'red' |
'black' |
Visual preset for the header and launcher. |
themeColors |
CcbotThemeColors |
{} |
Per-property color overrides on top of the selected theme. |
triggerLabel |
string |
'Ask the CCBot' |
Launcher button label. |
headerTitle / headerSubtitle |
string |
'CCBot' / 'AI assistant' |
Header text in the chat panel. |
welcomeTitle |
string |
'Explore the website' |
Welcome screen heading. |
welcomeMessage |
CcbotWelcomeMessage |
default message | Message shown under the welcome title. Remote config supports strings; React consumers may also pass a React element or component type. |
suggestedQuestionsLabel |
string |
'Suggested questions' |
Heading above suggested questions. |
about |
CcbotAboutConfig |
built-in sections | About-this-assistant dialog content. |
Remote configuration (automatic)
When auth is configured, the widget automatically fetches tenant-specific settings from:
GET …/ccbot-config — a sibling of the chat API base (e.g. https://example.com/services/ccbot-config when apiBaseUrl is https://example.com/services/chat)
using the same chat session token as ASK/VOTE. The response is a partial CcbotWidgetConfig JSON object (or {} if nothing is customized). Values from the server are merged on top of the config prop, which in turn overrides built-in defaults.
No extra props or flags are required. Integrators only need apiBaseUrl (or the package default) and auth.
downloadFilename
Controls the filename used by the download conversation action.
By default, downloaded conversations are named:
conversation-YYYY-MM-DD_HH-mm.txt
Pass a string for a fixed filename:
<CCbotWidget
auth={{ tokenEndpoint: '/ccbot/api/token' }}
config={{
downloadFilename: 'support-conversation',
}}
/>
This downloads as support-conversation.txt.
Pass a function for full control:
<CCbotWidget
auth={{ tokenEndpoint: '/ccbot/api/token' }}
config={{
downloadFilename: ({ timestamp }) => {
const date = timestamp.toISOString().slice(0, 10);
return `customer-chat-${date}.txt`;
},
}}
/>
Or extend the built-in default filename:
<CCbotWidget
auth={{ tokenEndpoint: '/ccbot/api/token' }}
config={{
downloadFilename: ({ defaultFilename }) => `portal-${defaultFilename}`,
}}
/>
Filenames are sanitized before download, and .txt is appended automatically when omitted.
defaultOpen & openBehavior
By default the widget renders collapsed to its launcher button. Set defaultOpen: true to render it open on the first visit.
<CCbotWidget
auth={{ tokenEndpoint: '/ccbot/api/token' }}
config={{
defaultOpen: true,
openBehavior: 'greet-once',
}}
/>
openBehavior controls what happens on subsequent visits (it has no effect when defaultOpen is false, since nothing ever auto-opens):
| Value | First visit | After the user closes it | Subsequent loads |
|---|---|---|---|
greet-once (default) |
Open | Stays closed | Closed |
remember |
Open | Restores the last state | Restores the last open/closed state the user left it in |
always |
Open | Re-opens on next load | Always open |
The open/closed state is persisted in localStorage under `<storagePrefix>:open` (namespaced and isolated via the top-level storagePrefix prop). It is only written when defaultOpen is enabled. No cookies are used. always persists nothing.
Note: Like all
configvalues,defaultOpenandopenBehaviorcan also be supplied via remote configuration, letting operators control auto-open behavior centrally.
pageScope — CcbotPageScopeConfig · Optional
Controls the ability to narrow the chatbot's knowledge to the content of the current page. When pageId is provided, the widget checks the Content Cloud backend to determine whether that page has been ingested and can be used for page-scoped search.
| Prop | Type | Default | Description |
|---|---|---|---|
pageId |
string |
— | A unique identifier for the current page (e.g. a node ID or slug). Used by the backend to scope results to this page's content. |
toggleEnabled |
boolean |
— | When true, a toggle control is shown in the widget allowing the user to switch between querying the current page or all content. |
defaultMode |
'page' | 'all' |
'page' |
The default scope mode when the widget first opens. |
auth — CcbotAuthConfig · Required
Configures how the widget obtains a Chat Session Token (CST) to authenticate with the Content Cloud chat service. You must provide one of the two options below.
Option A — Token endpoint URL
The widget will call your backend endpoint to retrieve a fresh token automatically.
auth={{ tokenEndpoint: '/ccbot/api/token' }}
| Prop | Type | Description |
|---|---|---|
tokenEndpoint |
string |
A URL (relative or absolute) on your backend that returns a Chat Session Token. The widget sends a POST request to this URL and expects a JSON response containing a token string. |
Option B — Custom token function
Provide an async function that resolves to a token string. Use this when you need full control over headers, caching, or token refresh logic.
auth={{
getToken: async () => {
const res = await fetch('/my/custom/token-endpoint');
const { token } = await res.json();
return token;
}
}}
| Prop | Type | Description |
|---|---|---|
getToken |
() => Promise<string> |
An async function that returns a Chat Session Token string. |
identity — CcbotIdentityConfig · Optional
Optional configuration for associating conversations with a specific user. Useful for personalising responses or tracking usage per user.
| Prop | Type | Description |
|---|---|---|
getUserId |
() => string |
A function that returns the current user's unique identifier (e.g. a user ID from your authentication system). Called when a conversation starts. |
Example:
identity={{
getUserId: () => currentUser.id,
}}
theme — CcbotTheme · Optional
Controls the widget's color theme. The widget ships built-in light and dark themes; this prop lets your React app switch between them declaratively.
| Value | Default | Description |
|---|---|---|
'light' |
✓ | Light theme. |
'dark' |
Dark theme. | |
'auto' |
Follows the user's prefers-color-scheme OS setting and updates live when it changes. |
Example — bind to your own theme state:
const [theme, setTheme] = useState<'light' | 'dark' | 'auto'>('auto');
return (
<CCbotWidget
auth={{ tokenEndpoint: '/ccbot/api/token' }}
theme={theme}
/>
);
See Dark mode under Styling for theming details and for the non-React escape hatch.
Full Example
import { CCbotWidget } from '@eworx-org/ccbot-react';
export default function App() {
return (
<CCbotWidget
apiBaseUrl="https://staging.contentcloud.com/services/chat"
storagePrefix="my_site_ccbot"
config={{
threadWelcomeAvatarLabel: 'Ask AI',
suggestedQuestionsAllContent: [
'What services do you offer?',
'How do I get started?',
],
suggestedQuestionsThisPage: [
'Summarise this page for me',
'What are the key points here?',
],
pageScope: {
pageId: '42',
toggleEnabled: true,
defaultMode: 'page',
},
}}
auth={{ tokenEndpoint: '/ccbot/api/token' }}
identity={{ getUserId: () => 'user-123' }}
/>
);
}
Implementing a Custom Token Endpoint (Non-Drupal Sites)
If your site does not use Drupal (which provides the /ccbot/api/token endpoint out of the box via the CCBot module), you need to implement your own backend endpoint that securely exchanges your Content Cloud API key for a Chat Session Token (CST).
Why a backend endpoint? The API key must never be exposed in client-side code. The token exchange must always happen server-side.
Request Flow
The flow is:
Browser → Your Backend Endpoint → Content Cloud Token Service → Your Backend → Browser
- The CCbotWidget (running in the browser) calls your backend endpoint to request a token.
- Your backend endpoint calls the Content Cloud Token Service, passing your API key.
- The Token Service responds with a short-lived Chat Session Token.
- Your backend returns the token to the widget.
- The widget uses the token to authenticate chat requests for the duration of the session.
Step 1 — Create a Widget-Facing Endpoint
Create an endpoint on your own backend and pass its URL to the widget:
<CCbotWidget
auth={{ tokenEndpoint: '/ccbot/api/token' }}
/>
When the widget needs a token, it sends this request to your endpoint:
POST /ccbot/api/token HTTP/1.1
Content-Type: application/json
The widget does not send a request body. Your endpoint should use your existing server-side context, such as the user's session cookie or authentication headers, if you need to decide whether the current visitor is allowed to receive a chat token.
Your endpoint should respond with JSON:
{
"token": "eyJ..."
}
If the request is not allowed, return an appropriate error status such as 401 or 403. If the token exchange fails, return a 5xx response or another status that matches your application's error handling.
Step 2 — Exchange Your API Key for a Token
From your backend, make an HTTP POST request to:
https://www.contentcloud.com/services/chat-token-service/exchange
The request must include:
- Method:
POST - Header:
X-Api-Key: YOUR_CONTENT_CLOUD_API_KEY
Your API key is provisioned when you register your site with Content Cloud. Keep it stored securely (e.g. in an environment variable or secrets manager) and never commit it to source control.
Step 3 — Handle the Content Cloud Response
On success, the Token Service returns an HTTP 200 response with a JSON body containing the Chat Session Token:
{
"token": "eyJ..."
}
Extract the token value from the response. This is the Chat Session Token (CST) you will return to the browser.
If the exchange fails (invalid API key, service unavailable, etc.), the Token Service will return a non-200 status code. Your endpoint should handle these cases gracefully and return an appropriate error response to the client.
Example Endpoint
The exact framework does not matter, but the endpoint should follow this shape:
app.post('/ccbot/api/token', async (req, res) => {
if (!isAllowedToUseChat(req)) {
res.status(403).json({ error: 'Not allowed' });
return;
}
const exchangeResponse = await fetch(
'https://www.contentcloud.com/services/chat-token-service/exchange',
{
method: 'POST',
headers: {
'X-Api-Key': process.env.CONTENT_CLOUD_API_KEY,
},
},
);
if (!exchangeResponse.ok) {
res.status(502).json({ error: 'Failed to fetch chat session token' });
return;
}
const data = await exchangeResponse.json();
if (!data?.token) {
res.status(502).json({ error: 'Invalid token response' });
return;
}
res.json({ token: data.token });
});
If your backend framework requires explicit response headers, use Content-Type: application/json.
If you need a different request method, custom request body, custom headers, or a different response shape, use auth.getToken instead of auth.tokenEndpoint. With auth.getToken, you fully control the browser-side fetch and only need to return the token string from that function.
Security Considerations
- Protect your endpoint. If your chat is intended only for authenticated users, require a valid session or authentication header before issuing a token.
- Do not cache tokens for too long. Chat Session Tokens are short-lived by design. Issue a fresh token per chat session rather than reusing one across sessions.
- Store the API key securely. Use environment variables or a secrets manager. Never hard-code it or expose it in client-side bundles.
Styling
The widget is rendered as a floating button fixed to the bottom-right corner of the viewport and opens as a resizable modal. No additional layout changes are required on the host page.
The widget mounts inside a Shadow DOM and bundles its own styles — there is no stylesheet to import. This isolation means the host page's CSS cannot bleed into the widget, and the widget's CSS cannot bleed onto the host.
There are two supported ways to customise the widget's appearance from your application:
- CSS custom properties (
--ccbot-*) — for theme tokens such as colors, typography, and corner radius. ::part()selectors — for targeted overrides on specific elements (the launcher button, modal, dialog, etc.).
Both work with plain CSS — no Tailwind, no JavaScript, no special build setup.
The widget renders into a custom element named <ccbot-widget>. All overrides are scoped to that selector.
1. Theme tokens (CSS variables)
Set any of the following variables on the host element (or any ancestor of it — they inherit through the shadow boundary). The widget reads them internally.
ccbot-widget {
--ccbot-primary: 280 80% 50%;
--ccbot-primary-foreground: 0 0% 100%;
--ccbot-radius: 0.25rem;
}
Color value format
Color tokens use the HSL triplet format: three space-separated numbers, without the hsl(...) wrapper, without commas, with % on saturation and lightness:
/* ✓ correct */
--ccbot-primary: 280 80% 50%;
/* ✗ all wrong */
--ccbot-primary: hsl(280 80% 50%);
--ccbot-primary: hsl(280, 80%, 50%);
--ccbot-primary: #7c3aed;
--ccbot-primary: rebeccapurple;
This format is required because the widget uses Tailwind's alpha-shorthand internally (e.g. hover states at reduced opacity). The widget composes the final color as hsl(var(--ccbot-primary) / <alpha>).
Available tokens
| Variable | Purpose |
|---|---|
--ccbot-font-family |
Primary font-family stack used for all widget text. Monospace elements (code blocks, inline code) continue to use the separate mono stack. |
--ccbot-background |
Widget surface background. |
--ccbot-foreground |
Default text color on the widget surface. |
--ccbot-primary |
Primary action color (launcher button, send button). |
--ccbot-primary-foreground |
Foreground color paired with --ccbot-primary. |
--ccbot-secondary |
Secondary action color. |
--ccbot-secondary-foreground |
Foreground paired with --ccbot-secondary. |
--ccbot-muted |
Muted background (suggested-question cards, etc.). |
--ccbot-muted-foreground |
Foreground paired with --ccbot-muted. |
--ccbot-accent |
Accent background (hover states). |
--ccbot-accent-foreground |
Foreground paired with --ccbot-accent. |
--ccbot-popover |
Modal panel background. |
--ccbot-popover-foreground |
Modal panel text color. |
--ccbot-card |
Card background. |
--ccbot-card-foreground |
Card text color. |
--ccbot-destructive |
Destructive action color. |
--ccbot-destructive-foreground |
Foreground paired with --ccbot-destructive. |
--ccbot-border |
Default border color. |
--ccbot-input |
Input border / surface color. |
--ccbot-ring |
Focus ring color. |
--ccbot-radius |
Base corner radius (length, e.g. 0.5rem). |
Dark mode
The widget ships a built-in dark theme. The recommended way to activate it from a React app is the theme prop:
<CCbotWidget
auth={{ tokenEndpoint: '/ccbot/api/token' }}
theme="dark"
/>
theme accepts:
| Value | Behavior |
|---|---|
'light' (default) |
Light theme. |
'dark' |
Dark theme. |
'auto' |
Follows the user's prefers-color-scheme OS setting and updates live when it changes. |
You can drive the prop from your own state — for example, a theme toggle in your app:
const [theme, setTheme] = useState<'light' | 'dark' | 'auto'>('auto');
return (
<CCbotWidget
auth={{ tokenEndpoint: '/ccbot/api/token' }}
theme={theme}
/>
);
Under the hood, the prop adds class="dark" to the <ccbot-widget> host element, which switches every --ccbot-* token to its dark-mode value. If you ever need to toggle this manually (e.g. from non-React code), you can still do so directly:
document.querySelector('ccbot-widget').classList.toggle('dark');
2. ::part() selectors
For overrides that go beyond theme tokens — geometry, shadows, custom backgrounds, focus styles — target the widget's named parts. The widget exposes a stable set of part attributes.
ccbot-widget::part(trigger) {
background: rebeccapurple;
box-shadow: 0 8px 24px rgba(102, 51, 153, 0.5);
}
ccbot-widget::part(trigger):hover {
transform: scale(1.15);
}
ccbot-widget::part(anchor) {
bottom: 2rem;
left: 2rem;
right: auto; /* move launcher to bottom-left */
}
ccbot-widget::part(dialog) {
border: 2px solid black;
border-radius: 0;
}
Available parts
| Part | Element |
|---|---|
anchor |
The fixed-position container holding the launcher button. |
trigger |
The launcher button itself (the outer pill that grows to reveal the label). |
trigger-badge |
The badge inside the launcher (the colored teardrop/circle that holds the icon). Style its background, border, and corner radius here. |
trigger-icon |
The bot/close SVG icon rendered inside the badge. |
trigger-label |
The label text that slides out of the launcher on first load, hover, and focus. |
modal |
The popover/dialog wrapper that opens above the trigger. |
content |
Alias on the same element as modal (set together). |
dialog |
The visible chat panel inside the modal. |
thread |
The conversation root. |
messages |
The scrollable messages viewport. |
composer |
The input area at the bottom of the thread. |
user-message |
A message bubble sent by the user. |
assistant-message |
A message bubble sent by the assistant. |
button |
Any button rendered inside the widget. |
tooltip |
The tooltip bubble. Style background/padding/text here. Setting z-index here also propagates to the underlying Radix positioning wrapper. |
Styling open vs. closed state
The launcher parts (trigger, trigger-badge, trigger-label) carry a
data-state attribute that is "open" while the modal is open and "closed"
otherwise. Chain it as an attribute selector to style each state independently
— for example, to give the badge a different background once the chat is
open:
/* Recolor the badge while the modal is open */
ccbot-widget::part(trigger-badge)[data-state="open"] {
background: rebeccapurple;
}
/* Restyle the launcher pill background in the open state */
ccbot-widget::part(trigger)[data-state="open"] {
background: transparent;
}
/* Tweak the slide-out label */
ccbot-widget::part(trigger-label) {
letter-spacing: 0.02em;
color: rebeccapurple;
}
Limitations of ::part()
These are CSS spec rules, not widget limitations:
- You cannot reach inside a part with a descendant combinator:
::part(dialog) .inneris invalid. - You cannot combine descendants inside the parens:
::part(anchor trigger)is invalid (use comma-separated rules instead). - You can chain pseudo-classes after the part:
::part(trigger):hover,::part(trigger):focus-visible. - You can chain attribute selectors on the part itself:
::part(button)[data-variant="ghost"].
If you need to style something that isn't currently exposed as a part, open an issue — the part list is the public theming API and is the right place to extend.
Example: complete custom theme
/* Brand the widget in your app's global stylesheet */
ccbot-widget {
--ccbot-font-family: "Inter", system-ui, sans-serif;
--ccbot-primary: 220 90% 56%;
--ccbot-primary-foreground: 0 0% 100%;
--ccbot-radius: 1rem;
}
ccbot-widget::part(trigger) {
box-shadow: 0 10px 30px hsl(220 90% 56% / 0.4);
}
ccbot-widget::part(dialog) {
border: 1px solid hsl(220 90% 56% / 0.2);
}
ccbot-widget::part(user-message) {
font-weight: 500;
}
The font file(s) must be loaded by the host page (via @font-face, Google Fonts <link>, etc.). The variable only supplies the font-family value.