--- title: Chat UI description: Install and compose Chat UI, with source references and integration guidance for engineers and agents. --- Chat UI is a family of components for conversations in a drawer, a panel, or a page. The components handle presentation and local interaction; your application owns messages, membership, persistence, and authorization. [Try the interactive showcase](/ui/chat): switch between a group chat and a direct message in the hero, or open the complete group drawer. All examples use synthetic conversation data. ## Start here - **Build a conversation:** install `@coss/chat` with `@coss/composer` and `@coss/avatar`, then use the [minimal example](#minimal-conversation). - **Adapt the full drawer:** install `@coss/chat-demo`, then read the [source map](#source-map) and [application responsibilities](#connect-your-application). - **Working as an agent:** read [this page as Markdown](/ui/docs/chat.md). The [chat registry source](/ui/r/chat.json) and [drawer example source](/ui/r/chat-demo.json) include file contents and dependencies. Use the exported TypeScript props as the API contract; the live showcase alone does not expose the implementation. ## Install Configure coss ui using [Get started](/ui/docs/get-started), then install the family: ```bash npx shadcn@latest add @coss/chat ``` This installs the chat source into your configured components directory under `chat/`, including its hooks and text helpers. It also installs the required coss components, Lucide icons, Base UI, and `motion@14.0.0`. The family uses React 19 and Tailwind CSS 4 in this repository. Keep Tailwind source scanning enabled for the installed directory. To start from the complete drawer example, including synthetic data and simulated actions: ```bash npx shadcn@latest add @coss/chat-demo ``` The example is separate from the component family. Its roles, moderation decisions, timers, and receipt aggregation are sample application code. Adapt them to your product's rules. The installed drawer example accepts a trigger renderer so you can place its controls anywhere on your page: ```tsx import { ChatDemo } from "@/components/chat-demo/chat-demo"; import { Button } from "@/components/ui/button"; import { DrawerTrigger } from "@/components/ui/drawer"; ( } onClick={() => select("conversation")} > Open chat )} /> ``` Use this composition inside a client component. Additional scenario values are exported from `chat-demo-state.ts`. ## Minimal conversation The example also uses Composer and Avatar. Install them explicitly alongside the chat family: ```bash npx shadcn@latest add @coss/chat @coss/composer @coss/avatar ``` This complete client component demonstrates plain-text sending with local state. A conversation needs a bounded height, a scroll controller, messages, and a controlled draft. Use the full drawer example for mentions, grouped messages, reactions, and focused replies. ```tsx "use client"; import { ChatConversation } from "@/components/chat/chat-conversation"; import { ChatMessage } from "@/components/chat/chat-message"; import { useChatScroll } from "@/components/chat/hooks/use-chat-scroll"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Composer, ComposerActionGroup, ComposerActions, ComposerInput, ComposerSubmit, type ComposerValue, getComposerText, } from "@/components/ui/composer"; import { useMediaQuery } from "@/hooks/use-media-query"; import { ArrowUpIcon } from "lucide-react"; import type { ReactElement } from "react"; import { useId, useState } from "react"; export function Conversation(): ReactElement { const [draft, setDraft] = useState([]); const hintId = useId(); const touchInput = useMediaQuery({ pointer: "coarse" }); const [messages, setMessages] = useState([ { id: 0, own: false, text: "The same chat components can live directly in a page. Try sending a message.", }, ]); const scroll = useChatScroll({ itemCount: messages.length }); return (
{messages.map((message) => ( ))}
{ const text = getComposerText(value).trim(); if (!text) return; scroll.atBottomRef.current = true; setMessages((current) => [...current, { id: current.length, own: true, text }]); setDraft([]); }}> } /> {!touchInput && (

Enter to send / Shift + Enter for a new line

)}
); } ``` In a real application, `onSubmit` starts your mutation. Clear the draft after accepting the message into your local send queue. Give each message a stable client ID so replacing a pending message with the server response does not remount it. ## Component map Import components directly from their files. There is no required global chat provider. | Area | Components | Responsibility | | --- | --- | --- | | Conversation | `ChatConversation`, `useChatScroll` | Scroll viewport, end following, jump to latest, overlay slot | | Message | `ChatMessage` | Incoming/outgoing row, avatar, author, grouping, actions, reactions, footer | | Surfaces | `ChatMessageBubble`, `ChatBubble`, `ChatMessageLayout` | Standalone previews and custom row compositions | | Content | `ChatMessageText`, `ChatCollapsibleMessage`, `ChatReplyReference` | Mention emphasis, long text, quoted message navigation | | Composer | `Composer`, `ComposerInput`, `ComposerPopup`, `ComposerList`, `ComposerItem`, `ComposerSubmit` | Shared coss Composer: structured draft, send, adaptive layout, and mentions | | Actions | `ChatMessageActionToolbar`, `ChatReactionPicker`, `ChatReactionChip` | Reply/menu controls, emoji selection, reactor list | | Focused reply | `ChatReplyPreview` | Move the selected message above the composer, dim the timeline, cancel/exit | | Timeline | `ChatDatePill`, `ChatUnreadDivider`, `ChatJumpToLatestButton` | Sticky date markers, unread boundary, latest navigation | | Activity | `ChatTypingIndicator`, `ChatDeliveryIndicator` | Typing and delivery presentation | | People | `ChatMemberList`, `ChatMemberSection`, `ChatMessageInfo` | Member rows and receipt groups with supplied actions | | States | `ChatEmptyState`, `ChatLoadingState`, `ChatStatusBar` | Joining, unavailable history, loading, connection feedback | ## Source map Paths below are relative to your configured components directory after installation. Adjust `@/components` and `@/hooks` in examples to match your aliases. In the Coss repository, the canonical sources live under `apps/ui/registry/default/`. | File | Read it for | | --- | --- | | `chat/chat-*.tsx` | Exported component props, required labels, slots, and defaults | | `chat/hooks/use-chat-scroll.ts` | Scroll options and controller methods | | `ui/composer.tsx` | Shared Composer exports; see the [Composer API](/ui/docs/components/composer) for its complete contract | | `chat-demo/chat-demo.tsx` | Main/nested drawers, quote navigation, confirmations, and reply enter/exit lifecycle | | `chat-demo/chat-demo-message.tsx` | Grouping, message slots, reaction anchors, long text, and deleted-message presentation | | `chat-demo/chat-demo-composer.tsx` | Responsive row limits, mention items, submit button, and tooltip composition | | `chat-demo/chat-demo-reply-focus.tsx` | Measured reply target and expanded-text snapshot | | `chat-demo/chat-demo-message-info.tsx` | Message preview and recipient groups | | `chat-demo/chat-demo-scenarios.tsx` | Loading, empty, and access-state presentations | | `chat-demo/chat-demo-state.ts`, `chat-demo/chat-demo-receipts.ts` | Synthetic fixtures and demo policy; replace with your application model | ## Messages and actions `ChatMessage` requires `side` and `children`. It accepts `author` and `avatar`, plus `actions`, `reactions`, and `footer` slots. Set `grouped` for consecutive messages from the same sender; it hides repeated incoming authors/avatars while preserving the avatar column. The first bubble uses a smaller upper corner on its sender's side; grouped bubbles use equal corners. Pass a bubble ref or native attributes through `bubbleProps`. Grouping and vertical spacing belong to the composition. The drawer breaks groups at date changes and deleted messages. It uses `pt-5` between senders, `pt-1` within a group, and `pt-3` after a grouped message with delivery metadata. Omit that padding immediately after a date or unread separator. Show incoming timestamps at the end of a group; outgoing delivery feedback remains per message. `ChatDeliveryIndicator` exposes a labelled image rather than a live region. Announce important send failures through a single application-owned status region. String children render through `ChatMessageText`. For mention emphasis, supply `ChatMessageText` with `mentions` containing known handles. Custom React content passes through unchanged. `ChatCollapsibleMessage` requires `text`, `side`, `showMoreLabel`, and `showLessLabel`. Its default `maxPreviewLength` is 500; shorter text has no expansion control. Use `renderText` to preserve mention formatting in both views. Choose uncontrolled `defaultOpen` or controlled `open`/`onOpenChange`, and keep that choice stable. Keep images and attachments outside this text-only component. Use `bubble={false}` for a deleted-message placeholder. Your application decides whether historical messages remain after membership changes. The toolbar reserves its width beside the bubble and appears on hover, keyboard focus, or coarse pointers. Popups dismiss on user scroll input in their scroll container. Automatic scrolling as messages arrive does not dismiss them. ### Reactions `ChatReactionPicker` takes `options` (`{ emoji, label }[]`), `selectedEmojis`, `onSelect`, and the accessible strings `triggerLabel`, `triggerTooltip`, and `popupLabel`. When composing it through `ChatMessageActionToolbar.reactionPicker`, set `inToolbar` and pass the callback's ref to `popupAnchor`. | Surface | Incoming message | Outgoing message | | --- | --- | --- | | Picker anchored to the toolbar | `popupAlign="end"` | `popupAlign="start"` | | Reactor popup anchored to its chip | `popupAlign="start"` | `popupAlign="end"` | `ChatReactionChip` is controlled through `selected`, `count`, and `onClick`; it does not update your reaction data. Supply `emoji` and an accessible `label` describing the action and count. Clicking or pressing Enter/Space toggles the reaction. With `reactorContent`, delayed hover, keyboard focus, or a long touch opens the list; also provide `reactorLabel`. The caller renders names, avatars, and a “You” marker, with a bounded scroll area for long lists. Set `animateEntrance` only for newly added chips. ## Composer and mentions Use the shared [Composer](/ui/docs/components/composer) with a controlled `ComposerValue`, `onValueChange`, and `onSubmit`. Convert accepted drafts with `getComposerText(value).trim()` and clear them with `[]`. The minimal example uses `ComposerInput render={