🚀 Headless React Chatbot Widget with TypeScript & TailwindCSS Support
A powerful, customizable React chatbot component that provides headless architecture with built-in hooks for modern chat experiences. Perfect for developers who want full control over their AI chatbot UI while leveraging robust state management.
🔍 Perfect for developers searching for:
- React chatbot component
- Headless chatbot widget
- TypeScript chatbot library
- TailwindCSS chat widget
- Customizable AI chat interface
- React hooks for chat functionality

- Complete UI flexibility while handling complex state management
- Build your own chat interface with any CSS framework (TailwindCSS, styled-components, etc.)
- Zero UI assumptions - you control every pixel
-
TypeScript Support: Full type safety and IntelliSense
-
React Hooks: Modern React patterns with useChatbot hook
-
TailwindCSS Ready: Pre-built examples with TailwindCSS classes
-
SSR Compatible: Works with Next.js, Remix, and other SSR frameworks
-
Minimal Dependencies: Lightweight bundle size
- 💾 Conversation Persistence: Automatic saving and loading of conversation history
- ✉️ Greeting Messages: Support for initial bot messages when starting a new chat
- 🔄 Conversation Management: Load, clear, and manage chat sessions with ease
- ⚠️ Error Handling: Built-in error state management
- 🔄 Real-time Updates: Smooth typing indicators and message updates
🎯 Why Choose This React Chatbot?
vs. Other React Chat Libraries
- ✅ Truly Headless: Unlike other chatbot widgets, we don't force any UI decisions
- ✅ TypeScript First: Built with TypeScript for better developer experience
- ✅ Modern React: Uses latest React patterns and hooks
- ✅ Framework Agnostic: Works with Next.js, Vite, Create React App, Remix
- ✅ Styling Freedom: Use TailwindCSS, CSS Modules, styled-components, or any CSS solution
- 🏢 Customer Support Chatbots - Add AI support to your SaaS
- 📚 Documentation Assistants - Help users navigate your docs
- 🛒 E-commerce Chat - Product recommendations and support
- 🎓 Educational Platforms - Interactive learning assistants
- 💼 Internal Tools - Employee help desks and knowledge bases
⭐ If this package helps you, please consider giving it a star on GitHub! ⭐
# npm
npm install @candoa/chatbot
# yarn
yarn add @candoa/chatbot
# pnpm
pnpm add @candoa/chatbot
Option 1: Complete AI Solution ❤️
For a complete AI chatbot solution with knowledge base training, analytics, and conversation management:
-
Learn more: candoa.app - See features, pricing, and demos
Features include:
- Train your AI on your own data
- Get your project ID
- Access conversation analytics
- Manage customer interactions
Option 2: Fork & Connect Your Backend
This package is open source! Feel free to fork it and connect to your own backend infrastructure.
import { useChatbot } from '@candoa/chatbot'
function MyChatbot() {
const { state, actions } = useChatbot('your-project-id', {
greetings: ['Hello! How can I assist you today?'],
})
return (
<div>
{/* Your custom chat UI using state and actions */}
{state.messages.map((message) => (
<div key={message.id}>{message.text}</div>
))}
<button onClick={() => actions.sendMessage('Hello')}>Send Message</button>
</div>
)
}
Quick Start with Styled UI
For a complete styled chatbot like shown in the demo:
import { ChatbotWidget } from '@candoa/chatbot'
import { useEffect, useRef } from 'react'
export function StyledChatbot() {
return (
<ChatbotWidget
projectId="your-project-id"
greetings={['Hello! How can I assist you today?']}
>
{({
isOpen,
messages,
error,
isTyping,
onSendMessage,
onToggleOpen,
onClose,
onClearMessages,
onClearError,
}) => {
const inputRef = useRef<HTMLInputElement>(null)
// Auto-focus input when chat opens
useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus()
}
}, [isOpen])
return (
<div className="relative">
{/* Chat Toggle Button */}
<button
onClick={onToggleOpen}
className="fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-slate-900 text-slate-50 shadow-lg ring-1 ring-slate-900/10 transition-all hover:bg-slate-800 hover:shadow-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2"
>
<svg
className="h-7 w-7"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M2.25 12.76c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.076-4.076a1.526 1.526 0 011.037-.443 48.282 48.282 0 005.68-.494c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z"
/>
</svg>
</button>
{isOpen && (
<div className="fixed bottom-24 right-6 z-50 w-96 rounded-lg border border-slate-200 bg-white shadow-xl">
{/* Chat Header */}
<div className="flex items-center justify-between border-b border-slate-200 p-4">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-slate-900">
<svg
className="h-4 w-4 text-slate-50"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/>
</svg>
</div>
<div>
<h3 className="text-sm font-semibold text-slate-900">
Customer Support
</h3>
<p className="text-xs text-slate-500">
Typically replies in minutes
</p>
</div>
</div>
<div className="flex items-center gap-1">
<button
onClick={onClearMessages}
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2"
title="Clear chat history"
>
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
</button>
<button
onClick={onClose}
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2"
>
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
{/* Chat Messages */}
<div className="h-[400px] space-y-4 overflow-y-auto p-4">
{messages.map((message) => (
<div
key={message.id}
className={`flex ${message.isUser ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[75%] rounded-lg px-3 py-2 text-sm ${
message.isUser
? 'bg-slate-900 text-slate-50'
: 'bg-slate-100 text-slate-900'
}`}
>
{message.text}
</div>
</div>
))}
{isTyping && (
<div className="flex justify-start">
<div className="rounded-lg bg-slate-100 px-3 py-2">
<div className="flex space-x-1">
<div className="h-2 w-2 animate-bounce rounded-full bg-slate-400"></div>
<div className="h-2 w-2 animate-bounce rounded-full bg-slate-400 [animation-delay:0.15s]"></div>
<div className="h-2 w-2 animate-bounce rounded-full bg-slate-400 [animation-delay:0.3s]"></div>
</div>
</div>
</div>
)}
</div>
{/* Error Message */}
{error && (
<div className="mx-4 mb-4 flex items-center justify-between rounded-md border border-red-200 bg-red-50 p-3 text-xs text-red-600">
<span>{error}</span>
<button
onClick={onClearError}
className="rounded px-2 py-1 text-xs transition-colors hover:bg-red-100"
>
Dismiss
</button>
</div>
)}
{/* Chat Input */}
<div className="border-t border-slate-200 p-4">
<form
className="flex w-full gap-2"
onSubmit={(e) => {
e.preventDefault()
const input = e.currentTarget.elements.namedItem(
'message',
) as HTMLInputElement
if (input.value.trim()) {
onSendMessage(input.value)
input.value = ''
}
}}
>
<input
ref={inputRef}
type="text"
name="message"
placeholder="Ask me anything..."
className="flex-1 rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-950 focus:ring-offset-2"
autoComplete="off"
/>
<button
type="submit"
className="inline-flex h-9 w-9 items-center justify-center rounded-md bg-slate-900 text-slate-50 transition-colors hover:bg-slate-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2"
>
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
/>
</svg>
</button>
</form>
</div>
</div>
)}
</div>
)
}}
</ChatbotWidget>
)
}
To use the chatbot with Candoa's AI solution, get your project ID from candoa.app:
- Sign up or log in to your Candoa CRM
- Click on your profile name in the top navigation
- In the profile dropdown, you'll see a Projects tab
- Under the Projects tab, you'll find your Project ID
The project ID connects your chatbot to your trained AI model and knowledge base.
Using Your Own Backend (Optional)
By default, the chatbot connects to Candoa's hosted AI service. If you prefer to use your own backend implementation, you can specify a custom API URL:
With the useChatbot hook:
const { state, actions } = useChatbot('your-project-id', {
greetings: ['Hello! How can I assist you today?'],
})
With the ChatbotWidget component:
<ChatbotWidget
projectId="your-project-id"
greetings={['Hello! How can I assist you today?']}
>
{/* Your render prop content */}
</ChatbotWidget>
Your backend needs to implement the chatbot API endpoints (/api/chatbot and /api/messages) to handle chat requests and conversation history.
The core hook that powers the chatbot functionality.
const { state, actions } = useChatbot(projectId, options)
-
projectId (string): Unique identifier for the project
-
options (object):
-
title (string, optional): Chat title
-
greetings (string[], optional): Initial greeting messages
-
icon (React component, optional): Icon component for the chat
-
initialConversationId (string, optional): Load a specific conversation initially
-
apiUrl (string, optional): Base URL for API calls to your backend (e.g., 'https://your-api-domain.com').
An object with two properties:
-
state (object):
-
isOpen (boolean): Whether the chat is open
-
messages (Message[]): Current chat messages
-
error (string | null): Error message, if any
-
isTyping (boolean): Whether the bot is "typing"
-
hasActiveConversation (boolean): Whether there is an active conversation
-
actions (object):
-
setIsOpen (function): Open or close the chat
-
sendMessage (function): Send a message to the bot
-
clearMessages (function): Clear the conversation history
-
clearError (function): Clear any error messages
-
loadConversation (function): Load a conversation by ID
A render prop component that provides a complete chatbot interface.
<ChatbotWidget projectId={projectId} {...options}>
{(renderProps) => (
// Your custom UI using renderProps
)}
</ChatbotWidget>
-
projectId (string): Unique identifier for the project
-
title (string, optional): Chat title
-
greetings (string[], optional): Initial greeting messages
-
icon (React component, optional): Icon component for the chat
-
apiUrl (string, optional)
-
children (function): Render prop function that receives the chatbot state and actions
The render prop function receives an object with the following properties:
-
isOpen (boolean): Whether the chat is open
-
messages (Message[]): Current chat messages
-
error (string | null): Error message, if any
-
isTyping (boolean): Whether the bot is "typing"
-
hasActiveConversation (boolean): Whether there is an active conversation
-
onSendMessage (function): Send a message to the bot
-
onToggleOpen (function): Toggle the chat open/closed state
-
onClose (function): Close the chat
-
onClearMessages (function): Clear the conversation history
-
onClearError (function): Clear any error messages
-
onLoadConversation (function): Load a conversation by ID
type Message = {
id: string
text: string
isUser: boolean
timestamp: Date
}
MIT © Candoa