Chat Widget React block for Tailwind CSS v4
v1.0.0Added in 1.14.0
A React support chat widget like Intercom or Crisp: a floating launcher that opens Home, Messages and Help, with an AI agent and a handoff.
Press the round launcher (a real button; its badge counts unread replies) and the panel grows out of its corner on a spring while the icon turns into a chevron. Home greets the visitor with the team's avatars and presence dots, the typical reply time, the most recent conversation and a help search that filters articles as you type. Messages lists every conversation with unread counts; Help groups articles by collection and opens them in place. Arrow keys, Home and End move between the three tabs. Send us a message starts a conversation: the AI agent greets you with quick reply chips, searches the help center while a shimmering line says so, and links the article it used. Talk to a person (in the notice bar or a chip) hands the conversation to a teammate, who joins and types back. Enter sends, Shift+Enter adds a line, the paperclip attaches files. End the conversation and a star rating asks how it went. New replies are announced politely to screen readers. Escape closes the panel and puts focus back on the launcher. On a phone the panel fills the screen with safe-area padding and the composer rides above the keyboard.
Chat Widget is a Pro React block in the React AI components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add chat-widget, together with Avatar, Button, Chat Message, Presence, Prompt Input and 11 more and the npm packages motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add chat-widgetChat Widget is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import ChatWidget from @/components/blocks/chat-widget.
import { ChatWidget, type ChatWidgetSendHandler } from "@/components/blocks/chat-widget"
const answer: ChatWidgetSendHandler = async (input, { conversation, mode, signal }) => {
const response = await fetch("/api/support", {
method: "POST",
body: JSON.stringify({ conversationId: conversation.id, mode, text: input.text }),
signal,
})
const data: { reply: string } = await response.json()
return { text: data.reply, quickReplies: [{ label: "Talk to a person", action: "handoff" }] }
}
export function SupportChat() {
return (
<ChatWidget
greeting="Hi Emma"
replyTime="under 5 minutes"
color="#4f46e5"
onSend={answer}
onRate={(rating, conversation) => console.log(conversation.id, rating)}
/>
)
}Props
greetingdefault"Hi there"ReactNodeThe big line at the top of the Home tab. Put the visitor's name in it when you know it.
introdefault"How can we help today?"ReactNodeThe line under the greeting.
replyTimedefault"a few minutes"stringHow fast the team answers, read as "We typically reply in a few minutes" on Home, in human conversations and in the handoff note.
botNamedefault"Support AI"stringThe AI agent's name in its greeting, its header and the screen reader announcements.
launcherLabeldefault""stringA text label next to the launcher icon, like "Chat with us". Empty keeps the round launcher.
teamdefault"CHAT_WIDGET_TEAM"ChatWidgetAgent[]The people behind the chat: { id, name, avatar?, role?, status?: "online" | "away" | "busy" | "offline" }. They show as a presence stack on Home, and the first online person answers human conversations.
articlesdefault"CHAT_WIDGET_ARTICLES"ChatWidgetArticle[]Help articles: { id, title, description?, collection?, body?, href?, readTime?, keywords? }. Searched on Home and Help, grouped by collection, opened in the panel (or in a new tab with href), and quoted by the pretend AI agent.
conversationsChatWidgetConversation[]Controlled conversations: { id, mode?, agentId?, messages, unread?, status?: "open" | "resolved", rating? }. A message is { id, from: "user" | "agent" | "bot" | "system", text, agentId?, createdAt?, attachments?, quickReplies?, articleIds? }; agent and bot text is markdown.
defaultConversationsdefault"CHAT_WIDGET_CONVERSATIONS"ChatWidgetConversation[]The starting conversations while uncontrolled. Pass [] for a visitor who never wrote in.
onConversationsChange(conversations: ChatWidgetConversation[]) => voidEvery change: a sent message, a reply, a handoff, a read conversation, a rating. Save it to keep the history.
quickRepliesdefault"CHAT_WIDGET_QUICK_REPLIES"ChatWidgetQuickReply[]The chips under the AI agent's greeting in a new conversation. A string is sent as a message; { label, action: "handoff" | "resolve" } calls a person in or ends the conversation.
modedefault"ai""ai" | "human"Who answers a new conversation. ai: the AI agent answers at once, with a Talk to a person notice and chip that hands over to the team. human: the team answers and the presence stack and reply time set expectations.
positiondefault"bottom-right""bottom-right" | "bottom-left"The corner of the launcher; the panel grows out of it.
strategydefault"fixed""fixed" | "absolute"fixed pins the widget to the viewport (the usual choice, render it once in your layout). absolute keeps it inside the nearest positioned container, for previews and app frames. The playground stage holds both.
offsetdefault20numberDistance in px from the screen edges, on top of the safe-area insets.
defaultOpendefaultfalsebooleanOpens the panel on mount without moving focus. Turn it on to see the Home tab.
openbooleanControlled open state of the panel.
onOpenChange(open: boolean) => voidRuns when the launcher, the close button or Escape opens or closes the panel.
defaultTabdefault"home""home" | "messages" | "help"The tab the panel starts on.
tab"home" | "messages" | "help"Controlled tab.
onTabChange(tab: ChatWidgetTab) => voidRuns when a tab is picked, or when Back leaves a conversation for Messages.
conversationIdstring | nullThe open conversation, controlled; null shows the tabs. Open a thread from a "Contact support" button elsewhere in the app.
defaultConversationIddefault""string | nullThe conversation open on mount while uncontrolled. The playground offers the two sample threads and opens the panel on them.
onConversationIdChange(id: string | null) => voidRuns when a conversation opens or Back closes it.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The launcher, the Home header wash, the visitor's bubbles, the AI mark and the send button.
colordefault""stringAny CSS color, your brand color. Replaces the tone everywhere; text on the fill turns black or white by lightness.
headerdefault"soft""soft" | "solid"The Home header: a soft wash of the color fading into the panel, or a solid fill like Intercom.
sizedefault"md""sm" | "md" | "lg"The launcher: 48 / 56 / 64px with a 20 / 24 / 28px icon. The panel sits 12px above it.
radiusdefault"lg""none" | "sm" | "md" | "lg"The panel's corners from 768px: 0 / 12 / 16 / 24px. On phones the panel fills the screen without corners.
allowAttachmentsdefaulttruebooleanThe composer's attach button, drop and paste. Files arrive in onSend as attachments and show in the visitor's bubble.
showRatingdefaulttruebooleanAsks for a 1 to 5 star rating when a conversation ends (That helped, or the End conversation button).
safeAreadefaulttruebooleanKeeps the launcher, the full-screen phone panel's header and its composer clear of the notch and the home indicator.
unreaddefault-1numberThe launcher badge, 0 hides it. -1 (the default) uses the sum of the conversations' unread counts; replies that arrive while the panel is closed add to it.
launcherIconReactNodeReplaces the chat bubble icon of the closed launcher.
logoReactNodeYour logo at the top left of the Home tab. Defaults to the AI agent's mark.
botAvatarReactNodeReplaces the sparkle in the AI agent's round mark (an image, your logo).
onSenddefault"pretendChatWidgetReply"(input: { text, attachments }, context: { conversation, mode, articles, team, labels, signal }) => ChatWidgetReply | Promise<ChatWidgetReply | void>Answers a message. Return the reply (a string or { text, from?, agentId?, quickReplies?, articleIds? }) and the widget shows the typing state until it arrives; return nothing and push replies into conversations yourself. The default pretends: the AI agent quotes the best matching article, a teammate acknowledges.
onHandoff(conversation: ChatWidgetConversation) => ChatWidgetReply | Promise<ChatWidgetReply | void>Talk to a person: notify your team and return their first message. Default a pretend greeting from the first online teammate.
onRate(rating: number, conversation: ChatWidgetConversation) => voidThe satisfaction rating, 1 to 5, when the visitor sends it.
onArticleOpen(article: ChatWidgetArticle) => voidRuns when an article opens, in the panel or in a new tab.
localedefault""stringThe locale of the relative times (2 hr. ago). Empty uses the UIProvider's, then the browser's.
labelsPartial<ChatWidgetLabels>Every string: tab names, the launcher and close names, the reply time sentence, the AI notice and Talk to a person, the handoff and joined notes, the rating copy and its five star labels, the pretend replies and the screen reader announcements.
classNamesPartial<Record<"launcher" | "badge" | "panel" | "header" | "hero" | "card" | "tabs" | "tab" | "thread" | "chips" | "composer" | "rating" | "article", string>>Extra classes for the inner parts.
classNamestringExtra classes for the root, the layer that holds the launcher and the panel.
Parts
pretendChatWidgetReply
functionThe default onSend: waits a moment (aborts with the signal), then the AI agent answers from the best matching article with That helped and Talk to a person chips, or a teammate acknowledges in human mode. Wrap it to fall back on it.
searchChatWidgetArticles
functionsearchChatWidgetArticles(query, articles): the articles whose title, description, collection or keywords hold every word, title matches first. The Home and Help search use it.
DEFAULT_CHAT_WIDGET_LABELS
constantThe English strings. Type: ChatWidgetLabels.
CHAT_WIDGET_TEAM
constantThe sample team: Olivia Bennett, Marcus Reed (online) and Priya Shah (away).
CHAT_WIDGET_ARTICLES
constantSeven sample help articles in Account, Billing and Getting started.
CHAT_WIDGET_CONVERSATIONS
constantTwo sample conversations: an unread billing thread with Priya and a resolved, rated AI answer.
CHAT_WIDGET_QUICK_REPLIES
constantThe default chips under the AI agent's greeting.
ChatWidgetConversation
type{ id, mode?, agentId?, messages: ChatWidgetMessage[], unread?, status?, rating? }.
ChatWidgetMessage
type{ id, from: "user" | "agent" | "bot" | "system", text, agentId?, createdAt?, attachments?, quickReplies?, articleIds? }.
ChatWidgetReply
typeWhat onSend and onHandoff return: a string, or { text, from?: "bot" | "agent", agentId?, quickReplies?, articleIds? }.
ChatWidgetSendHandler
typeThe type of onSend.
Examples
Brand color, solid header
A custom color paints the launcher, the Home header, the visitor's bubbles and the send button.
The team answers
mode="human": no AI agent, the presence stack and the reply time set expectations, a teammate types back.
Closed, with an unread badge
Bottom left, a large launcher with a text label; the badge counts unread replies and pops in on a spring.
Satisfaction rating
A resolved conversation ends with a star rating; onRate receives the score and the conversation.
Controlled, with your backend
open and conversationId are controlled from buttons outside; onSend returns the reply and its quick replies.
Dependencies
Files
- components/blocks/chat-widget.tsx
- components/blocks/chat-widget-data.ts
FAQ
Is the React Chat Widget block free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Chat Widget?
Run npx wingo-ui@latest add chat-widget. It also adds the Wingo UI items it builds on (Avatar, Button, Chat Message, Presence, Prompt Input and 11 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/chat-widget.
How does Chat Widget behave on phones?
On a phone the panel fills the screen with safe-area padding and the composer rides above the keyboard.