# Chat Widget

> 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.

- Type: React block (AI Kit)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add chat-widget`
- Docs: https://wingo-ui.com/components/chat-widget

## Interaction

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.

## Installation

```bash
npx wingo-ui@latest add chat-widget
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/chat-widget` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/blocks/chat-widget.tsx`
- `components/blocks/chat-widget-data.ts`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Chat Message](https://wingo-ui.com/components/chat-message) ([Markdown](https://wingo-ui.com/components/chat-message.md))
- [Presence](https://wingo-ui.com/components/presence) ([Markdown](https://wingo-ui.com/components/presence.md))
- [Prompt Input](https://wingo-ui.com/components/prompt-input) ([Markdown](https://wingo-ui.com/components/prompt-input.md))
- [Rating](https://wingo-ui.com/components/rating) ([Markdown](https://wingo-ui.com/components/rating.md))
- [Reasoning Indicator](https://wingo-ui.com/components/reasoning-indicator) ([Markdown](https://wingo-ui.com/components/reasoning-indicator.md))
- [Search Input](https://wingo-ui.com/components/search-input) ([Markdown](https://wingo-ui.com/components/search-input.md))
- [Suggestion Chips](https://wingo-ui.com/components/suggestion-chips) ([Markdown](https://wingo-ui.com/components/suggestion-chips.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useKeyboardInset](https://wingo-ui.com/components/use-keyboard-inset) ([Markdown](https://wingo-ui.com/components/use-keyboard-inset.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Relative time](https://wingo-ui.com/components/relative-time) ([Markdown](https://wingo-ui.com/components/relative-time.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
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

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `greeting?` | `ReactNode` | `"Hi there"` | The big line at the top of the Home tab. Put the visitor's name in it when you know it. |
| `intro?` | `ReactNode` | `"How can we help today?"` | The line under the greeting. |
| `replyTime?` | `string` | `"a few minutes"` | How fast the team answers, read as "We typically reply in a few minutes" on Home, in human conversations and in the handoff note. |
| `botName?` | `string` | `"Support AI"` | The AI agent's name in its greeting, its header and the screen reader announcements. |
| `launcherLabel?` | `string` | `""` | A text label next to the launcher icon, like "Chat with us". Empty keeps the round launcher. |
| `team?` | `ChatWidgetAgent[]` | `"CHAT_WIDGET_TEAM"` | 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. |
| `articles?` | `ChatWidgetArticle[]` | `"CHAT_WIDGET_ARTICLES"` | 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. |
| `conversations?` | `ChatWidgetConversation[]` |  | 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. |
| `defaultConversations?` | `ChatWidgetConversation[]` | `"CHAT_WIDGET_CONVERSATIONS"` | The starting conversations while uncontrolled. Pass [] for a visitor who never wrote in. |
| `onConversationsChange?` | `(conversations: ChatWidgetConversation[]) => void` |  | Every change: a sent message, a reply, a handoff, a read conversation, a rating. Save it to keep the history. |
| `quickReplies?` | `ChatWidgetQuickReply[]` | `"CHAT_WIDGET_QUICK_REPLIES"` | 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. |
| `mode?` | `"ai" \| "human"` | `"ai"` | 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. |
| `position?` | `"bottom-right" \| "bottom-left"` | `"bottom-right"` | The corner of the launcher; the panel grows out of it. |
| `strategy?` | `"fixed" \| "absolute"` | `"fixed"` | 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. |
| `offset?` | `number` | `20` | Distance in px from the screen edges, on top of the safe-area insets. |
| `defaultOpen?` | `boolean` | `false` | Opens the panel on mount without moving focus. Turn it on to see the Home tab. |
| `open?` | `boolean` |  | Controlled open state of the panel. |
| `onOpenChange?` | `(open: boolean) => void` |  | Runs when the launcher, the close button or Escape opens or closes the panel. |
| `defaultTab?` | `"home" \| "messages" \| "help"` | `"home"` | The tab the panel starts on. |
| `tab?` | `"home" \| "messages" \| "help"` |  | Controlled tab. |
| `onTabChange?` | `(tab: ChatWidgetTab) => void` |  | Runs when a tab is picked, or when Back leaves a conversation for Messages. |
| `conversationId?` | `string \| null` |  | The open conversation, controlled; null shows the tabs. Open a thread from a "Contact support" button elsewhere in the app. |
| `defaultConversationId?` | `string \| null` | `""` | The conversation open on mount while uncontrolled. The playground offers the two sample threads and opens the panel on them. |
| `onConversationIdChange?` | `(id: string \| null) => void` |  | Runs when a conversation opens or Back closes it. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The launcher, the Home header wash, the visitor's bubbles, the AI mark and the send button. |
| `color?` | `string` | `""` | Any CSS color, your brand color. Replaces the tone everywhere; text on the fill turns black or white by lightness. |
| `header?` | `"soft" \| "solid"` | `"soft"` | The Home header: a soft wash of the color fading into the panel, or a solid fill like Intercom. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | The launcher: 48 / 56 / 64px with a 20 / 24 / 28px icon. The panel sits 12px above it. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg"` | `"lg"` | The panel's corners from 768px: 0 / 12 / 16 / 24px. On phones the panel fills the screen without corners. |
| `allowAttachments?` | `boolean` | `true` | The composer's attach button, drop and paste. Files arrive in onSend as attachments and show in the visitor's bubble. |
| `showRating?` | `boolean` | `true` | Asks for a 1 to 5 star rating when a conversation ends (That helped, or the End conversation button). |
| `safeArea?` | `boolean` | `true` | Keeps the launcher, the full-screen phone panel's header and its composer clear of the notch and the home indicator. |
| `unread?` | `number` | `-1` | The 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. |
| `launcherIcon?` | `ReactNode` |  | Replaces the chat bubble icon of the closed launcher. |
| `logo?` | `ReactNode` |  | Your logo at the top left of the Home tab. Defaults to the AI agent's mark. |
| `botAvatar?` | `ReactNode` |  | Replaces the sparkle in the AI agent's round mark (an image, your logo). |
| `onSend?` | `(input: { text, attachments }, context: { conversation, mode, articles, team, labels, signal }) => ChatWidgetReply \| Promise<ChatWidgetReply \| void>` | `"pretendChatWidgetReply"` | 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) => void` |  | The satisfaction rating, 1 to 5, when the visitor sends it. |
| `onArticleOpen?` | `(article: ChatWidgetArticle) => void` |  | Runs when an article opens, in the panel or in a new tab. |
| `locale?` | `string` | `""` | The locale of the relative times (2 hr. ago). Empty uses the UIProvider's, then the browser's. |
| `labels?` | `Partial<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. |
| `classNames?` | `Partial<Record<"launcher" \| "badge" \| "panel" \| "header" \| "hero" \| "card" \| "tabs" \| "tab" \| "thread" \| "chips" \| "composer" \| "rating" \| "article", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Extra classes for the root, the layer that holds the launcher and the panel. |

## Parts

### pretendChatWidgetReply (function)

The 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 (function)

searchChatWidgetArticles(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 (constant)

The English strings. Type: ChatWidgetLabels.

### CHAT_WIDGET_TEAM (constant)

The sample team: Olivia Bennett, Marcus Reed (online) and Priya Shah (away).

### CHAT_WIDGET_ARTICLES (constant)

Seven sample help articles in Account, Billing and Getting started.

### CHAT_WIDGET_CONVERSATIONS (constant)

Two sample conversations: an unread billing thread with Priya and a resolved, rated AI answer.

### CHAT_WIDGET_QUICK_REPLIES (constant)

The 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 (type)

What onSend and onHandoff return: a string, or { text, from?: "bot" | "agent", agentId?, quickReplies?, articleIds? }.

### ChatWidgetSendHandler (type)

The 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.

## See also

- [Chat Message](https://wingo-ui.com/components/chat-message) ([Markdown](https://wingo-ui.com/components/chat-message.md))
- [Prompt Input](https://wingo-ui.com/components/prompt-input) ([Markdown](https://wingo-ui.com/components/prompt-input.md))
- [Suggestion Chips](https://wingo-ui.com/components/suggestion-chips) ([Markdown](https://wingo-ui.com/components/suggestion-chips.md))
- [Presence](https://wingo-ui.com/components/presence) ([Markdown](https://wingo-ui.com/components/presence.md))
- [FAB](https://wingo-ui.com/components/fab) ([Markdown](https://wingo-ui.com/components/fab.md))

---

Canonical: https://wingo-ui.com/components/chat-widget
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
