I built a headless React chatbot widget for full UI control

1 day ago 4

🚀 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

Candoa Chatbot Demo

  • 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:

  1. Sign up or log in to your Candoa CRM
  2. Click on your profile name in the top navigation
  3. In the profile dropdown, you'll see a Projects tab
  4. 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

Read Entire Article