An open-source, embeddable AI chat widget powered by Claude. Drop it into any website with a single script tag.
Try it live at pmds.info
- Floating chat bubble with toggle
- Markdown rendering (bold, italic, links)
- Dark mode (light, dark, auto)
- Conversation persistence (localStorage)
- Rate limiting (KV-based, per IP)
- Accessible (WCAG 2.1 AA, Lighthouse 98/100)
- Responsive (mobile-first, works at 320px+)
- Configurable (title, colors, system prompt, theme)
- Lightweight (~150KB gzipped JS + 3KB CSS)
cd worker
pnpm install
cp .dev.vars.example .dev.vars # Add your Anthropic API key
pnpm dev # Starts on http://localhost:8787cd widget
pnpm install
pnpm dev # Starts on http://localhost:51733. Open http://localhost:5173 and test the chat
Import and use ChatWidget directly in your React app:
# Install (when published to npm)
npm install claudius-chat-widgetimport { ChatWidget } from "claudius-chat-widget";
import "claudius-chat-widget/style.css";
function App() {
return (
<ChatWidget
apiUrl="https://your-worker.workers.dev"
title="Support"
subtitle="Ask me anything"
theme="auto"
accentColor="#0057a3"
position="bottom-right"
/>
);
}For non-React sites, use the IIFE bundle with window.ClaudiusConfig:
<script>
window.ClaudiusConfig = {
apiUrl: "https://your-worker.workers.dev",
title: "Support",
subtitle: "Ask me anything",
theme: "auto",
accentColor: "#0057a3",
};
</script>
<link rel="stylesheet" href="/path/to/claudius.css" />
<script src="/path/to/claudius.iife.js"></script>To auto-update instead of self-hosting, load the version-pinned CDN channel
(@1 resolves the latest v1.x release; requires allowing cdn.jsdelivr.net
in your CSP). See DEPLOY.md.
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/PMDevSolutions/Claudius@1/cdn/claudius.css"
/>
<script src="https://cdn.jsdelivr.net/gh/PMDevSolutions/Claudius@1/cdn/claudius.iife.js"></script>Both the React component and embed script accept these options:
| Option | Default | Description |
|---|---|---|
apiUrl |
(required) | URL of your Cloudflare Worker |
title |
"Chat" |
Header title |
subtitle |
"Ask me anything" |
Header subtitle |
welcomeMessage |
"Hi! How can I help you today?" |
First message shown |
placeholder |
"Type your message..." |
Input placeholder |
persistMessages |
true |
Save chat history to sessionStorage (survives page navigation, clears on tab close) |
storageKeyPrefix |
"claudius:messages" |
Storage key prefix; set to a unique value per widget when embedding multiple widgets on one page |
requestTimeoutMs |
30000 |
Per-attempt request timeout in ms. The widget aborts and surfaces a retryable timeout error. Set to 0 to disable. |
theme |
"light" |
Color scheme: "light", "dark", or "auto" |
accentColor |
"#2563eb" |
Primary brand color override |
position |
"bottom-right" |
Widget position: "bottom-right", "bottom-left", "top-right", "top-left" |
translations |
(built-in) | Custom UI strings (React component only) |
triggers |
undefined |
Proactive triggers that auto-open the widget or show a greeting bubble. See Proactive Triggers below. |
Configure the widget to auto-open or pop a greeting bubble when a visitor hits a condition. The widget remembers a dismissal in sessionStorage, so once the visitor closes the auto-opened chat or dismisses the bubble, no further triggers fire that session.
<ChatWidget
apiUrl="..."
triggers={[
// Pop a greeting bubble after 30s on any page
{ on: "time", seconds: 30, action: { greeting: "Need a hand?" } },
// Auto-open after the visitor scrolls 80% down the pricing page
{
on: "scroll",
percent: 80,
matchUrl: "/pricing",
action: "open",
},
// Exit-intent greeting only on the contact page
{
on: "exit-intent",
matchUrl: /\/contact/,
action: { greeting: "Have a quick question before you go?" },
},
// Greet immediately when someone lands on /docs
{
on: "url",
pattern: "/docs",
action: { greeting: "Looking for something specific in the docs?" },
},
]}
/>Each trigger has an on type, an action, and an optional matchUrl to scope to specific pages. matchUrl (and url-trigger pattern) accepts a substring string (case-insensitive) or a RegExp.
on |
Extra fields | When it fires |
|---|---|---|
"time" |
seconds: number |
After N seconds on the page |
"scroll" |
percent: number (0--100) |
Once the visitor scrolls past N% of the page |
"exit-intent" |
-- | When the mouse leaves the viewport via the top edge |
"url" |
pattern: string | RegExp |
Immediately on mount if the current URL matches |
action |
Behavior |
|---|---|
"open" |
Auto-opens the chat window |
{ greeting: string } |
Pops a dismissable greeting bubble next to the toggle button |
Edit worker/src/system-prompt.ts to customize the AI's personality, knowledge base, services, pricing, and FAQ. This is where you make the chatbot yours.
Edit widget/tailwind.config.ts to change brand colors, fonts, and border radii. Colors use CSS custom properties so you can also override them at runtime via accentColor.
The widget supports three theme modes:
"light"(default) -- Light background, dark text"dark"-- Dark background, light text"auto"-- Follows the user's OS preference viaprefers-color-scheme
The worker includes KV-based rate limiting to protect against API abuse:
- 10 requests/minute per IP
- 50 requests/hour per IP
Create a KV namespace for rate limiting:
cd worker
npx wrangler kv namespace create RATE_LIMITCopy the output ID into wrangler.toml:
[[kv_namespaces]]
binding = "RATE_LIMIT"
id = "your-namespace-id"
preview_id = "your-preview-namespace-id"For local development, wrangler automatically creates a local KV store.
cd worker
npx wrangler login
npx wrangler secret put ANTHROPIC_API_KEY
npx wrangler deploySet ALLOWED_ORIGIN in the Cloudflare dashboard (Workers > Settings > Variables) to your production domain.
cd widget
pnpm build:embedOutput: dist/claudius.iife.js and dist/claudius.css
Host these files on your site or a CDN, then add the embed snippet to your HTML.
cd widget
pnpm test # Unit + integration (Vitest)
pnpm test:coverage # Coverage report (target: 80%+)
pnpm e2e:install # One-time: download Chromium for Playwright
pnpm e2e # End-to-end (Playwright, against `pnpm dev`)
pnpm e2e:ui # Playwright UI modeThe E2E suite mocks **/api/chat via page.route() so the worker doesn't need to be running, and builds the embed bundle once in globalSetup to exercise it via <script src> and <claudius-chat> web component.
- Widget: React 18, TypeScript, Tailwind CSS, Vite
- Worker: Cloudflare Workers, Hono, Anthropic SDK, KV
- AI Model: Claude Haiku 4.5
Contributions are welcome. Before opening a pull request, please read:
- CONTRIBUTING.md -- setup, branch naming, PR process, and code style
- CODE_OF_CONDUCT.md -- community expectations
- SECURITY.md -- how to report security issues responsibly
Quick checklist before submitting a PR:
- Branch from
mainusing afeat/,fix/,docs/, orchore/prefix - Add tests for new functionality
- Run
pnpm testin bothwidget/andworker/ - Write a clear PR title (under 70 chars) and description
Released under the MIT License.
