# Cometchat Placement

> Production integration patterns -- how to add CometChat as a route, modal, drawer, embedded panel, or widget in an existing project. Teaches Claude WHERE to put chat.

- Skill: `cometchat/cometchat-placement` (Agent Skill)
- Install (CLI): `npx skillmds@latest add cometchat/cometchat-placement`
- Raw SKILL.md: https://api.skillmd.com/api/skills/cometchat/cometchat-placement/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- License: MIT
- Author: cometchat (https://skillmd.com/u/cometchat)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/cometchat/cometchat-placement

---


> **Ground truth:** `@cometchat/chat-uikit-react@^6` components composed for placement + `docs/ui-kit/react`. **Official docs:** https://www.cometchat.com/docs/ui-kit/react/overview · **Docs MCP:** `claude mcp add --transport http cometchat-docs https://www.cometchat.com/docs/mcp` (or fetch the URL directly without MCP). Verify symbols against the installed package/source before relying on them.

## Purpose

This skill teaches you WHERE to put CometChat in an existing project. It covers five placement patterns: route, modal, drawer, embedded panel, and floating widget. Each pattern includes step-by-step instructions and complete code examples.

**This skill is framework-AGNOSTIC.** It uses generic instructions like "create a page at the framework's route location" and "add a route entry to the project's router." The framework-specific details (file paths, SSR handling, env var prefixes) come from the framework skill and the `cometchat-core` skill.

**Before using this skill:**
- Read `cometchat-core` for initialization, login, CSS, and provider patterns
- Read `cometchat-components` for component names, props, and composition patterns

---

## "What are you building?" -- placement recommendation

Use this table to recommend a placement based on what the user is building. If the user says "add chat to my app" without specifying where, ask them what they are building and use this table.

| User intent | Recommended placement | Experience composition |
|---|---|---|
| Messaging app | Route (full page) | Multi-conversation (CometChatConversations + MessageHeader + MessageList + MessageComposer) |
| Marketplace / platform | Drawer on product page + `/messages` route | Single thread (drawer) + Multi-conversation (route) |
| SaaS / dashboard | Modal from navbar + `/messages` route | Single thread (modal) + Multi-conversation (route) |
| Social / community | Route (tabs) | Full messenger (CometChatConversations + CallLogs + Users + Groups with tabs) |
| Support / helpdesk | Floating widget | Widget (use CLI) |
| Just exploring | Demo (replace home page) | Multi-conversation |

---

## Visual reference — experience layouts

When presenting experience options to the user, describe these layouts
or share the ASCII art so they can visualize what each looks like.

### Multi-conversation (Experience 1)

Two-pane layout: conversation list on the left, active chat thread on the right.

```
┌─────────────────────────┬───────────────────────────────────────┐
│ Chats               Q   │ Richard Ray               v  c  i     │
├─────────────────────────┼───────────────────────────────────────┤
│                         │                                       │
│ (RR) Richard Ray  3:45  │           ╭─────────────────────╮     │
│      Is it still up..   │           │ Hi, is the watch    │     │
│                         │           │ still up for sale?  │     │
│ (SB) Sarah Beth   3:40  │           ╰────────── 4:56 PM ─╯      │
│      Sure! Sending ..   │                                       │
│                         │ ╭─────────────────╮                   │
│ (RA) Robert Allen 3:38  │ │ Yes, it is      │                   │
│      Thanks! Looks ..   │ │ available.      │                   │
│                         │ ╰─ 4:56 PM ──────╯                    │
│ (SG) Sam Game     3:30  │                                       │
│      Sending them ..    │           ╭─────────────────────╮     │
│                         │           │ Can I see a couple  │     │
│ (SF) Scott F.     3:22  │           │ of pictures?        │     │
│      I will look ..     │           ╰────────── 4:56 PM ─╯      │
│                         │                                       │
│ (EP) Evan Parker  3:15  │ ╭─────────────────╮                   │
│      Hey, did you ..    │ │ Sure! Sending   │                   │
│                         │ │ them over now.  │                   │
│ (JP) John Paul    3:10  │ ╰─ 4:56 PM ──────╯                    │
│      Sounds good        │                                       │
│                         │           ╭─────────────────────╮     │
│ (LK) Linda Kay    3:05  │           │ Thanks! Looks good. │     │
│      See you there      │           ╰────────── 4:56 PM ─╯      │
│                         ├───────────────────────────────────────┤
│                         │ Type a message...                 >   │
└─────────────────────────┴───────────────────────────────────────┘
```

Best for: messaging apps, team chat, inboxes, dedicated chat sections.

### Single thread (Experience 2)

One chat window — no conversation list. Shows a direct chat with one user or group.

```
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│ (RR) Richard Ray                              v    c    i       │
│      . Online                                                   │
│                                                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│                        ╭───────────────────────────────╮        │
│                        │ Hi, is the watch still up     │        │
│                        │ for sale?          4:56 PM vv │        │
│                        ╰───────────────────────────────╯        │
│                                                                 │
│ ╭───────────────────────╮                                       │
│ │ Yes, it is available. │                                       │
│ ╰── 4:56 PM ───────────╯                                        │
│                                                                 │
│                        ╭───────────────────────────────╮        │
│                        │ Awesome! Can I see a couple   │        │
│                        │ of pictures?       4:56 PM vv │        │
│                        ╰───────────────────────────────╯        │
│                                                                 │
│ ╭────────────────────────────────╮                              │
│ │ Sure! Sending them over now.   │                              │
│ ╰── 4:56 PM ────────────────────╯                               │
│                                                                 │
│                        ╭───────────────────────────────╮        │
│                        │ Thanks! Looks good. 4:56 PM vv│        │
│                        ╰───────────────────────────────╯        │
│                                                                 │
│ ╭─────────────╮                                                 │
│ │ Thank you!  │                                                 │
│ ╰── 4:56 PM ─╯                                                  │
│                                                                 │
├─────────────────────────────────────────────────────────────────┤
│ +  m  e  a     Type a message...                            >   │
└─────────────────────────────────────────────────────────────────┘
```

Best for: marketplace chat, embedded consult, support, one-on-one conversations.

### Full messenger (Experience 3)

Two-pane layout like Experience 1, plus a bottom tab bar for switching
between Chats, Calls, Users, and Groups.

```
┌─────────────────────────┬───────────────────────────────────────┐
│ Chats               Q   │ Richard Ray               v  c  i     │
├─────────────────────────┼───────────────────────────────────────┤
│                         │                                       │
│ (RR) Richard Ray  3:45  │           ╭─────────────────────╮     │
│      How much extra..   │           │ How much extra are  │     │
│                         │           │ we talking for the  │     │
│ (SB) Sarah Beth   3:40  │           │ direct flight?      │     │
│      That sounds w..    │           ╰────────── 4:56 PM ─╯      │
│                         │                                       │
│ (RA) Robert Allen 3:38  │ ╭──────────────────────╮              │
│      4:56 PM            │ │ It is $50 more. Save │              │
│                         │ │ a couple of hours.   │              │
│ (SG) Sam Game     3:30  │ ╰─ 4:56 PM ───────────╯               │
│      Sending them ..    │                                       │
│                         │           ╭─────────────────────╮     │
│ (SF) Scott F.     3:22  │           │ That sounds worth   │     │
│      I will look ..     │           │ it. Let us do it.   │     │
│                         │           ╰────────── 4:56 PM ─╯      │
│ (EP) Evan Parker  3:15  │                                       │
│      Hey, did you ..    │ ╭──────────────────────╮              │
│                         │ │ Great, I will send   │              │
│                         │ │ you the details.     │              │
│                         │ ╰─ 4:56 PM ───────────╯               │
│                         ├───────────────────────────────────────┤
│                         │ Type a message...                 >   │
├─────────────────────────┼───────────────────────────────────────┤
│ Ch   Ca   Us   Gr       │                                       │
└─────────────────────────┴───────────────────────────────────────┘
```

Best for: social apps, community platforms, dating apps, full-featured
chat products.

---

> **Composer choice for narrow/two-pane panes:** the canonical React v6 sample apps use **`CometChatCompactMessageComposer`** (not `CometChatMessageComposer`) in the two-pane messages view — it's the kit's space-optimized composer for a sidebar-width pane (`cometchat-uikit-react-v6/sample-app/src/components/CometChatMessages/CometChatMessages.tsx:85`). Both compile and work; prefer `CometChatCompactMessageComposer` for the split/two-pane layouts below and `CometChatMessageComposer` for full-width single-thread layouts.

## Height & scroll — the message-view column MUST be bounded

Every header+list+composer example below puts the three components as **direct
flex children** of a `display:flex; flexDirection:column` column. That works ONLY
if the column has a bounded height — i.e. the full chain (`html`/`body`/`#root` →
…→ this column) uses a definite **`height`** (e.g. `#root { height: 100vh }`),
never `min-height`, and every flex ancestor of `CometChatMessageList` has
`minHeight: 0`. Delete the Vite/CRA starter `#root { min-height: 100vh; max-width: … }`.

If the list still won't scroll (most common symptom), or you wrap the list in its
own `<div>` for custom chrome/tabs, you hit the kit's auto-injected `.cometchat`
element — see **`cometchat-react-patterns` → "Container height (and the flex-shrink
trap)" Bug 3**: give the list its own wrapper class with BOTH `.your-list-wrapper
{ flex: 1 1 0; min-height: 0; height: 100%; overflow: hidden }` (the **explicit
`height: 100%` is required** — a flex-grow-only height is indefinite, so the kit's
injected child collapses to content height and clips) AND a child rule
`.your-list-wrapper > .cometchat { height: 100%; overflow: hidden }` (inline styles
can't target the injected child). Those two rules are the single most common fix for
"I set heights everywhere and it still won't scroll."

---

## Thread replies — hidden by default in every example below

Every `<CometChatMessageList ...>` in the placement patterns below
includes `hideReplyInThreadOption`. The kit's default (`false`) puts a
**"Reply in Thread"** entry in every message's action menu — but that
entry only works if the integrator has wired up a thread panel
(`CometChatThreadHeader` + a scoped `CometChatMessageList` +
`CometChatMessageComposer` with `parentMessageId`). If the thread
panel isn't wired (the case for a simple drawer, widget, modal, or
single-thread experience), the option is still visible and clicking it
silently does nothing — confusing UX.

Default: **threads hidden**. To enable threads for an experience that
actually has the side-panel plumbing:

1. Remove `hideReplyInThreadOption` from the main `<CometChatMessageList>`
2. Add `onThreadRepliesClick` to capture the thread message
3. Render the thread panel (see `cometchat-components` § Threading for
   the full pattern — `CometChatThreadHeader` + scoped `MessageList` +
   scoped `MessageComposer` with `parentMessageId`)

---

## Route placement

The most common pattern. Chat gets its own page in the app, accessible via navigation.

### Steps

#### 1. Set up CometChatProvider at the app root

The provider (from `cometchat-core`) should wrap the entire app or the chat route's layout. This ensures init and login happen once, not per-navigation.

- **Read the project's existing layout/root component first.** Look for the outermost wrapper (e.g., `App.tsx`, `layout.tsx`, `root.tsx`).
- Add the `CometChatProvider` inside the existing layout, wrapping the router outlet or children.
- Import `@cometchat/chat-uikit-react/css-variables.css` at the app root CSS file if not already imported.

#### 2. Create a chat page component

Create a new file (e.g., `ChatPage.tsx` or `MessagesPage.tsx`) at the framework's conventional page location:

- React (Vite): `src/pages/ChatPage.tsx` or `src/ChatPage.tsx`
- Next.js (App Router): `app/chat/page.tsx`
- Next.js (Pages Router): `pages/chat.tsx`
- Astro: `src/pages/chat.astro` (with a React island)
- React Router: `app/routes/chat.tsx`

#### 3. Implement the page

Choose the experience composition from `cometchat-components`:

**Two-pane (most common for routes):**

```tsx
// ChatPage.tsx
import { useState } from "react";
import {
  CometChatConversations,
  CometChatMessageHeader,
  CometChatMessageList,
  CometChatMessageComposer,
} from "@cometchat/chat-uikit-react";
import { CometChat } from "@cometchat/chat-sdk-javascript";

export default function ChatPage() {
  const [selectedUser, setSelectedUser] = useState<CometChat.User>();
  const [selectedGroup, setSelectedGroup] = useState<CometChat.Group>();

  function handleConversationClick(conversation: CometChat.Conversation) {
    const entity = conversation.getConversationWith();
    if (entity instanceof CometChat.User) {
      setSelectedUser(entity);
      setSelectedGroup(undefined);
    } else if (entity instanceof CometChat.Group) {
      setSelectedUser(undefined);
      setSelectedGroup(entity);
    }
  }

  return (
    <div style={{ display: "flex", height: "100vh" }}>
      <div style={{ width: "360px", borderRight: "1px solid #eee" }}>
        <CometChatConversations onItemClick={handleConversationClick} />
      </div>
      <div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
        {(selectedUser || selectedGroup) ? (
          <>
            {selectedUser && <CometChatMessageHeader user={selectedUser} />}
            {selectedGroup && <CometChatMessageHeader group={selectedGroup} />}
            {selectedUser && <CometChatMessageList user={selectedUser} hideReplyInThreadOption />}
            {selectedGroup && <CometChatMessageList group={selectedGroup} hideReplyInThreadOption />}
            {selectedUser && <CometChatMessageComposer user={selectedUser} />}
            {selectedGroup && <CometChatMessageComposer group={selectedGroup} />}
          </>
        ) : (
          <div style={{
            flex: 1,
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            color: "#999",
          }}>
            Select a conversation to start chatting
          </div>
        )}
      </div>
    </div>
  );
}
```

**Full messenger (tabs -- for standalone messaging sections):**

```tsx
// MessagesPage.tsx
import { useState } from "react";
import {
  CometChatConversations,
  CometChatCallLogs,
  CometChatUsers,
  CometChatGroups,
  CometChatMessageHeader,
  CometChatMessageList,
  CometChatMessageComposer,
} from "@cometchat/chat-uikit-react";
import { CometChat } from "@cometchat/chat-sdk-javascript";

type Tab = "chats" | "calls" | "users" | "groups";

export default function MessagesPage() {
  const [activeTab, setActiveTab] = useState<Tab>("chats");
  const [selectedUser, setSelectedUser] = useState<CometChat.User>();
  const [selectedGroup, setSelectedGroup] = useState<CometChat.Group>();

  function selectUser(user: CometChat.User) {
    setSelectedUser(user);
    setSelectedGroup(undefined);
  }
  function selectGroup(group: CometChat.Group) {
    setSelectedUser(undefined);
    setSelectedGroup(group);
  }

  return (
    <div style={{ display: "flex", height: "100vh" }}>
      <div style={{ width: "360px", display: "flex", flexDirection: "column", borderRight: "1px solid #eee" }}>
        <nav style={{ display: "flex", borderBottom: "1px solid #eee" }}>
          {(["chats", "calls", "users", "groups"] as Tab[]).map((tab) => (
            <button
              key={tab}
              onClick={() => setActiveTab(tab)}
              style={{
                flex: 1,
                padding: "12px 0",
                border: "none",
                background: "none",
                cursor: "pointer",
                fontWeight: activeTab === tab ? 700 : 400,
                borderBottom: activeTab === tab ? "2px solid var(--cometchat-primary-color, #3399ff)" : "2px solid transparent",
              }}
            >
              {tab.charAt(0).toUpperCase() + tab.slice(1)}
            </button>
          ))}
        </nav>
        <div style={{ flex: 1, overflow: "hidden" }}>
          {activeTab === "chats" && (
            <CometChatConversations
              onItemClick={(conv) => {
                const entity = conv.getConversationWith();
                if (entity instanceof CometChat.User) selectUser(entity);
                else if (entity instanceof CometChat.Group) selectGroup(entity);
              }}
            />
          )}
          {activeTab === "calls" && <CometChatCallLogs />}
          {activeTab === "users" && <CometChatUsers onItemClick={selectUser} />}
          {activeTab === "groups" && <CometChatGroups onItemClick={selectGroup} />}
        </div>
      </div>
      <div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
        {selectedUser && (
          <>
            <CometChatMessageHeader user={selectedUser} />
            <CometChatMessageList user={selectedUser} hideReplyInThreadOption />
            <CometChatMessageComposer user={selectedUser} />
          </>
        )}
        {selectedGroup && (
          <>
            <CometChatMessageHeader group={selectedGroup} />
            <CometChatMessageList group={selectedGroup} hideReplyInThreadOption />
            <CometChatMessageComposer group={selectedGroup} />
          </>
        )}
        {!selectedUser && !selectedGroup && (
          <div style={{
            flex: 1,
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            color: "#999",
          }}>
            Select a conversation to start chatting
          </div>
        )}
      </div>
    </div>
  );
}
```

#### 4. Wire the route into the project's router

**Read the project's existing routing setup first.** Do not assume a pattern. Look for:

- **React Router:** `createBrowserRouter()`, `<Routes>`, `<Route>` in `App.tsx` or a routes file
- **Next.js App Router:** `app/` directory -- just creating the page file at `app/chat/page.tsx` IS the route
- **Next.js Pages Router:** `pages/` directory -- creating `pages/chat.tsx` IS the route
- **Astro:** `src/pages/` directory -- creating `src/pages/chat.astro` IS the route
- **React Router v7:** File-based routing in `app/routes/` or manual routes in `app/routes.ts`

For manual routers (React Router), add a route entry:

```tsx
// Example: adding to an existing createBrowserRouter
{
  path: "/chat",
  element: <ChatPage />,
}
```

For file-based routers (Next.js, Astro, React Router v7), creating the file at the right path is sufficient.

#### 5. Add a navigation link

**Read the project's existing navbar/sidebar first.** Find the component that renders navigation links (could be `Navbar.tsx`, `Sidebar.tsx`, `Header.tsx`, `Nav.tsx`, or inline in a layout).

Add a "Messages" or "Chat" link alongside the existing links:

```tsx
// Example: adding to an existing nav component
<Link to="/chat">Messages</Link>
// or
<a href="/chat">Messages</a>
```

Match the existing link style. If the nav uses icons, add a chat/message icon. If it uses a specific `NavLink` or `Link` component, use the same one.

#### 6. Import CSS

Check if `@cometchat/chat-uikit-react/css-variables.css` is already imported at the app root. If not, add it to the root CSS file or root layout:

```css
/* In globals.css or index.css at the app root */
@import "@cometchat/chat-uikit-react/css-variables.css";
```

---

## Modal placement

A centered overlay for quick one-off messages. Use when chat is a secondary action (e.g., "message this user" from a profile page).

### When to use modal vs. drawer

- **Modal:** Quick, one-off messages. User sends a message and closes. No ongoing conversation visible.
- **Drawer:** Ongoing conversation. User keeps the drawer open while browsing the main app. Better for marketplace/support contexts.

### Steps

#### 1. Create a ChatModal component

```tsx
// ChatModal.tsx
import { useEffect, useState } from "react";
import {
  CometChatMessageHeader,
  CometChatMessageList,
  CometChatMessageComposer,
} from "@cometchat/chat-uikit-react";
import { CometChat } from "@cometchat/chat-sdk-javascript";

interface ChatModalProps {
  isOpen: boolean;
  onClose: () => void;
  targetUserId?: string;
  targetGroupId?: string;
}

export function ChatModal({ isOpen, onClose, targetUserId, targetGroupId }: ChatModalProps) {
  const [user, setUser] = useState<CometChat.User>();
  const [group, setGroup] = useState<CometChat.Group>();
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    if (!isOpen) return;
    setLoading(true);

    if (targetUserId) {
      CometChat.getUser(targetUserId)
        .then((u) => {
          setUser(u);
          setGroup(undefined);
          setLoading(false);
        })
        .catch(() => setLoading(false));
    } else if (targetGroupId) {
      CometChat.getGroup(targetGroupId)
        .then((g) => {
          setUser(undefined);
          setGroup(g);
          setLoading(false);
        })
        .catch(() => setLoading(false));
    }
  }, [isOpen, targetUserId, targetGroupId]);

  if (!isOpen) return null;

  return (
    <div
      style={{
        position: "fixed",
        inset: 0,
        zIndex: 1000,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      }}
    >
      {/* Backdrop */}
      <div
        onClick={onClose}
        style={{
          position: "absolute",
          inset: 0,
          backgroundColor: "rgba(0, 0, 0, 0.5)",
        }}
      />

      {/* Modal content */}
      <div
        style={{
          position: "relative",
          width: "min(600px, 90vw)",
          height: "min(700px, 80vh)",
          backgroundColor: "var(--cometchat-background-color-01, #fff)",
          borderRadius: "var(--cometchat-border-radius-lg, 12px)",
          overflow: "hidden",
          display: "flex",
          flexDirection: "column",
          boxShadow: "0 20px 60px rgba(0, 0, 0, 0.3)",
        }}
      >
        {/* Close button */}
        <button
          onClick={onClose}
          style={{
            position: "absolute",
            top: 8,
            right: 8,
            zIndex: 10,
            background: "none",
            border: "none",
            fontSize: 20,
            cursor: "pointer",
            padding: "4px 8px",
          }}
          aria-label="Close chat"
        >
          X
        </button>

        {loading ? (
          <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }}>
            Loading...
          </div>
        ) : (
          <>
            {user && <CometChatMessageHeader user={user} />}
            {group && <CometChatMessageHeader group={group} />}
            <div style={{ flex: 1, overflow: "hidden" }}>
              {user && <CometChatMessageList user={user} hideReplyInThreadOption />}
              {group && <CometChatMessageList group={group} hideReplyInThreadOption />}
            </div>
            {user && <CometChatMessageComposer user={user} />}
            {group && <CometChatMessageComposer group={group} />}
          </>
        )}
      </div>
    </div>
  );
}
```

#### 2. Wire the trigger

**Read the project's components to find the right trigger point.** This could be:

- A "Message" button on a user profile page
- A "Contact Seller" button on a product card
- A chat icon in a navbar
- A "Send Message" link in a user list

```tsx
// Example: adding a chat button to an existing product card
import { useState } from "react";
import { ChatModal } from "./ChatModal";

function ProductCard({ product }) {
  const [showChat, setShowChat] = useState(false);

  return (
    <div>
      {/* existing product card content */}
      <button onClick={() => setShowChat(true)}>
        Message Seller
      </button>
      <ChatModal
        isOpen={showChat}
        onClose={() => setShowChat(false)}
        targetUserId={product.sellerId}
      />
    </div>
  );
}
```

#### 3. CometChatProvider placement

The `CometChatProvider` (or equivalent init logic) MUST be at the app root, NOT inside the modal. If init is inside the modal, it re-runs every time the modal opens, causing flicker and reconnection delays.

---

## Drawer placement

A side panel that slides in from the right. Better than a modal for ongoing conversations because the user can keep it open while browsing.

> ⚠️ **Known kit-side traps in narrow containers (≤ ~480px). Mitigate, don't ignore (ENG-35702/03/04/06):**
>
> 1. **Unread-count badges clip at the right edge in 320px panes** — kit's conversation-list item doesn't reserve trailing padding for the pill. Workaround until the kit fix lands: target `.cometchat-conversation-list-item__badge { margin-right: 8px; flex-shrink: 0; }` in your overrides, OR widen the drawer to ≥ 480px on the inner content (use `width: min(420px, 100vw)` + 8px inner padding).
> 2. **`Delete Conversation?` confirmation card overflows the list pane** — kit renders the dialog inside the list column instead of portalling to a top-level scrim. There's no clean CSS workaround today; if the integration depends on right-click delete, raise the drawer width above ~480px so the card fits inside.
> 3. **Composer attachment popover (`+` button) anchors to the wrong ancestor** — opens at viewport bottom-right instead of above the trigger when mounted inside a drawer/modal. Workaround: ensure the drawer container has `position: relative` (NOT just `position: fixed`) on its inner card so absolute popovers anchor correctly. If the popover still misaligns, mount the composer in a full-screen-on-mobile variant (the `right: -100%` recipe from §"Recipe — breakpoint-aware drawer").
> 4. **Voice recorder bubble UI is broken at kit v6.x** — the in-bubble recorder shows visual artifacts AND the pause-resume timer restarts at 00:00 instead of continuing. There is no client-side workaround; if voice messages are part of the integration's core surface, document the limitation for the customer and consider hiding the voice button (`<CometChatMessageComposer hideVoiceRecordingButton={true} />`) until the kit fix ships.
> 5. **`CometChatMessageList` does NOT auto-resubscribe on conversation switch** — passing a new `user` or `group` prop keeps the old listeners. **Required workaround when switching conversations inside a single drawer/modal:** pass `key={user?.getUid() ?? group?.getGuid()}` to force a remount + listener re-subscribe. Without it, switching from Alice → Bob silently shows Alice's messages with Bob's header. (ENG-35702)
> 6. **`CometChatMessageList` may render BLANK on conversations with unread messages** — kit-side data-path bug, no header or empty-state, just blank. No client-side workaround today; if the customer reports a blank list, capture the screenshot + UID/GUID + unread count and file a kit ticket. (ENG-35702 kit-side blocker)
>
> These are kit-side bugs filed in Linear (ENG-35702, 35703, 35704, 35706); the workarounds above are interim until the next kit release.

### Steps

#### 1. Create a ChatDrawer component

```tsx
// ChatDrawer.tsx
import { useEffect, useState } from "react";
import {
  CometChatMessageHeader,
  CometChatMessageList,
  CometChatMessageComposer,
} from "@cometchat/chat-uikit-react";
import { CometChat } from "@cometchat/chat-sdk-javascript";

interface ChatDrawerProps {
  isOpen: boolean;
  onClose: () => void;
  targetUserId?: string;
  targetGroupId?: string;
}

export function ChatDrawer({ isOpen, onClose, targetUserId, targetGroupId }: ChatDrawerProps) {
  const [user, setUser] = useState<CometChat.User>();
  const [group, setGroup] = useState<CometChat.Group>();
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    if (!isOpen) return;
    setLoading(true);

    if (targetUserId) {
      CometChat.getUser(targetUserId)
        .then((u) => {
          setUser(u);
          setGroup(undefined);
          setLoading(false);
        })
        .catch(() => setLoading(false));
    } else if (targetGroupId) {
      CometChat.getGroup(targetGroupId)
        .then((g) => {
          setUser(undefined);
          setGroup(g);
          setLoading(false);
        })
        .catch(() => setLoading(false));
    }
  }, [isOpen, targetUserId, targetGroupId]);

  return (
    <>
      {/* Backdrop */}
      {isOpen && (
        <div
          onClick={onClose}
          style={{
            position: "fixed",
            inset: 0,
            zIndex: 999,
            backgroundColor: "rgba(0, 0, 0, 0.3)",
          }}
        />
      )}

      {/* Drawer */}
      {/*
        IMPORTANT: do NOT animate with `transform: translateX(...)`.
        `transform` on an element creates a new containing block for
        `position: fixed` descendants (per the CSS spec), so every
        fixed-positioned popover that CometChat renders inside the drawer
        — message options menu, emoji picker, file preview, reactions
        popover, thread panel — becomes anchored to the transformed
        drawer instead of the viewport. The result is popovers that
        appear clipped, offset, or drift as the drawer animates. Animate
        the `right` offset instead; no transform, no containing-block
        takeover, fixed popovers stay anchored to the viewport.
      */}
      <div
        style={{
          position: "fixed",
          top: 0,
          // Breakpoint-aware: full-width on mobile (≤ 640px), 400px on desktop.
          // The "-100%" closed offset slides the drawer off-screen at any width,
          // so mobile-full-screen works without recomputing the offset (ENG-35715).
          right: isOpen ? 0 : "-100%",
          bottom: 0,
          width: "min(400px, 100vw)",
          maxWidth: "100vw",
          zIndex: 1000,
          backgroundColor: "var(--cometchat-background-color-01, #fff)",
          boxShadow: "-4px 0 20px rgba(0, 0, 0, 0.15)",
          display: "flex",
          flexDirection: "column",
          transition: "right 0.3s ease-in-out",
        }}
      >
        {/* Header with close button */}
        <div style={{
          display: "flex",
          alignItems: "center",
          justifyContent: "space-between",
          padding: "8px 12px",
          borderBottom: "1px solid #eee",
        }}>
          <span style={{ fontWeight: 600 }}>Chat</span>
          <button
            onClick={onClose}
            style={{
              background: "none",
              border: "none",
              fontSize: 18,
              cursor: "pointer",
              padding: "4px 8px",
            }}
            aria-label="Close chat"
          >
            X
          </button>
        </div>

        {/* Chat content */}
        {loading ? (
          <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }}>
            Loading...
          </div>
        ) : (
          <>
            {user && <CometChatMessageHeader user={user} />}
            {group && <CometChatMessageHeader group={group} />}
            <div style={{ flex: 1, overflow: "hidden" }}>
              {user && <CometChatMessageList user={user} hideReplyInThreadOption />}
              {group && <CometChatMessageList group={group} hideReplyInThreadOption />}
            </div>
            {user && <CometChatMessageComposer user={user} />}
            {group && <CometChatMessageComposer group={group} />}
          </>
        )}
      </div>
    </>
  );
}
```

#### 2. Wire the trigger

Same approach as the modal -- find the right trigger point in the existing project:

```tsx
import { useState } from "react";
import { ChatDrawer } from "./ChatDrawer";

function UserProfile({ userId }) {
  const [showChat, setShowChat] = useState(false);

  return (
    <div>
      {/* existing profile content */}
      <button onClick={() => setShowChat(true)}>
        Chat with this user
      </button>
      <ChatDrawer
        isOpen={showChat}
        onClose={() => setShowChat(false)}
        targetUserId={userId}
      />
    </div>
  );
}
```

#### 3. Multi-conversation drawer variant

For a drawer that shows the full conversation list (not just a single thread):

```tsx
// ConversationDrawer.tsx -- shows conversation list + message view
import { useState } from "react";
import {
  CometChatConversations,
  CometChatMessageHeader,
  CometChatMessageList,
  CometChatMessageComposer,
} from "@cometchat/chat-uikit-react";
import { CometChat } from "@cometchat/chat-sdk-javascript";

interface ConversationDrawerProps {
  isOpen: boolean;
  onClose: () => void;
}

export function ConversationDrawer({ isOpen, onClose }: ConversationDrawerProps) {
  const [selectedUser, setSelectedUser] = useState<CometChat.User>();
  const [selectedGroup, setSelectedGroup] = useState<CometChat.Group>();

  function handleConversationClick(conversation: CometChat.Conversation) {
    const entity = conversation.getConversationWith();
    if (entity instanceof CometChat.User) {
      setSelectedUser(entity);
      setSelectedGroup(undefined);
    } else if (entity instanceof CometChat.Group) {
      setSelectedUser(undefined);
      setSelectedGroup(entity);
    }
  }

  const hasSelection = selectedUser || selectedGroup;

  return (
    <>
      {isOpen && (
        <div
          onClick={onClose}
          style={{ position: "fixed", inset: 0, zIndex: 999, backgroundColor: "rgba(0,0,0,0.3)" }}
        />
      )}
      {/*
        Animate the `right` offset, never `transform: translateX(...)` —
        `transform` creates a new containing block, which re-anchors
        CometChat's fixed-positioned popovers (emoji picker, message
        options, file preview, thread panel) to the drawer instead of
        the viewport and makes them misalign.
      */}
      <div
        style={{
          position: "fixed",
          top: 0,
          right: isOpen ? 0 : "-720px",  // off-screen by widest width when closed
          bottom: 0,
          width: hasSelection ? "720px" : "360px",
          maxWidth: "100vw",
          zIndex: 1000,
          backgroundColor: "var(--cometchat-background-color-01, #fff)",
          boxShadow: "-4px 0 20px rgba(0,0,0,0.15)",
          display: "flex",
          transition: "right 0.3s ease-in-out, width 0.3s ease-in-out",
        }}
      >
        {/* Conversation list */}
        <div style={{ width: "360px", borderRight: hasSelection ? "1px solid #eee" : "none", display: "flex", flexDirection: "column" }}>
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "8px 12px", borderBottom: "1px solid #eee" }}>
            <span style={{ fontWeight: 600 }}>Messages</span>
            <button onClick={onClose} style={{ background: "none", border: "none", fontSize: 18, cursor: "pointer" }} aria-label="Close">&times;</button>
          </div>
          <div style={{ flex: 1 }}>
            <CometChatConversations onItemClick={handleConversationClick} />
          </div>
        </div>

        {/* Message view */}
        {hasSelection && (
          <div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
            {selectedUser && <CometChatMessageHeader user={selectedUser} />}
            {selectedGroup && <CometChatMessageHeader group={selectedGroup} />}
            <div style={{ flex: 1, overflow: "hidden" }}>
              {selectedUser && <CometChatMessageList user={selectedUser} hideReplyInThreadOption />}
              {selectedGroup && <CometChatMessageList group={selectedGroup} hideReplyInThreadOption />}
            </div>
            {selectedUser && <CometChatMessageComposer user={selectedUser} />}
            {selectedGroup && <CometChatMessageComposer group={selectedGroup} />}
          </div>
        )}
      </div>
    </>
  );
}
```

---

## Floating widget

A button-in-the-corner chat overlay. Available on every page of the app
without a dedicated route. Common for support widgets, helpdesk chat,
"contact us" overlays.

**When to use:** chat is a secondary concern (not the core product), and
should be accessible from anywhere without navigating. **When NOT to use:**
if chat is central to the app, use a route placement instead — widgets
don't scale to inbox-style usage.

### Steps

#### 1. Create the ChatWidget component

```tsx
// src/components/ChatWidget.tsx (or components/ChatWidget.tsx in Next.js)
import { useState } from "react";
import {
  CometChatConversations,
  CometChatMessageHeader,
  CometChatMessageList,
  CometChatMessageComposer,
} from "@cometchat/chat-uikit-react";
import { CometChat } from "@cometchat/chat-sdk-javascript";

export default function ChatWidget() {
  const [open, setOpen] = useState(false);
  const [selected, setSelected] = useState<CometChat.User | CometChat.Group>();

  function handleConversationClick(conv: CometChat.Conversation) {
    const entity = conv.getConversationWith();
    if (entity instanceof CometChat.User || entity instanceof CometChat.Group) {
      setSelected(entity);
    }
  }

  return (
    <>
      {/* Floating trigger button — always visible */}
      <button
        type="button"
        aria-label={open ? "Close chat" : "Open chat"}
        onClick={() => setOpen((v) => !v)}
        style={{
          position: "fixed",
          bottom: 24,
          right: 24,
          width: 56,
          height: 56,
          borderRadius: "50%",
          background: "var(--cometchat-primary-color, #6c63ff)",
          color: "white",
          border: "none",
          cursor: "pointer",
          fontSize: 24,
          boxShadow: "0 4px 16px rgba(0, 0, 0, 0.15)",
          zIndex: 9999,
        }}
      >
        {open ? "×" : "💬"}
      </button>

      {/* Widget panel — overlay, not a full-page drawer */}
      {open && (
        <div
          style={{
            position: "fixed",
            bottom: 96,  // leave room for the button
            right: 24,
            width: "min(380px, calc(100vw - 48px))",
            height: "min(600px, calc(100vh - 120px))",
            background: "var(--cometchat-background-color-01, white)",
            border: "1px solid var(--cometchat-border-color-light, #eee)",
            borderRadius: 12,
            boxShadow: "0 8px 32px rgba(0, 0, 0, 0.2)",
            display: "flex",
            flexDirection: "column",
            overflow: "hidden",
            zIndex: 9998,
          }}
        >
          {selected ? (
            <>
              <button
                onClick={() => setSelected(undefined)}
                style={{ alignSelf: "flex-start", background: "none", border: "none", pad

…(truncated)
