Ground truth:
@cometchat/chat-uikit-react@^6components 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-corefor initialization, login, CSS, and provider patterns - Read
cometchat-componentsfor 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(notCometChatMessageComposer) 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; preferCometChatCompactMessageComposerfor the split/two-pane layouts below andCometChatMessageComposerfor 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:
- Remove
hideReplyInThreadOptionfrom the main<CometChatMessageList> - Add
onThreadRepliesClickto capture the thread message - Render the thread panel (see
cometchat-components§ Threading for the full pattern —CometChatThreadHeader+ scopedMessageList+ scopedMessageComposerwithparentMessageId)
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
CometChatProviderinside the existing layout, wrapping the router outlet or children. - Import
@cometchat/chat-uikit-react/css-variables.cssat 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.tsxorsrc/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):
// 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 />
</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):
// 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}
=> 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
=> {
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 />}
{activeTab === "groups" && <CometChatGroups />}
</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>inApp.tsxor a routes file - Next.js App Router:
app/directory -- just creating the page file atapp/chat/page.tsxIS the route - Next.js Pages Router:
pages/directory -- creatingpages/chat.tsxIS the route - Astro:
src/pages/directory -- creatingsrc/pages/chat.astroIS the route - React Router v7: File-based routing in
app/routes/or manual routes inapp/routes.ts
For manual routers (React Router), add a route entry:
// 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:
// 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:
/* 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
// 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
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
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
// 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 => setShowChat(true)}>
Message Seller
</button>
<ChatModal
isOpen={showChat}
=> 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):
- 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 (usewidth: min(420px, 100vw)+ 8px inner padding).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.- 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 hasposition: relative(NOT justposition: 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 (theright: -100%recipe from §"Recipe — breakpoint-aware drawer").- 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.CometChatMessageListdoes NOT auto-resubscribe on conversation switch — passing a newuserorgroupprop keeps the old listeners. Required workaround when switching conversations inside a single drawer/modal: passkey={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)CometChatMessageListmay 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
// 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
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
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:
import { useState } from "react";
import { ChatDrawer } from "./ChatDrawer";
function UserProfile({ userId }) {
const [showChat, setShowChat] = useState(false);
return (
<div>
{/* existing profile content */}
<button => setShowChat(true)}>
Chat with this user
</button>
<ChatDrawer
isOpen={showChat}
=> setShowChat(false)}
targetUserId={userId}
/>
</div>
);
}
3. Multi-conversation drawer variant
For a drawer that shows the full conversation list (not just a single thread):
// 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
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 style={{ background: "none", border: "none", fontSize: 18, cursor: "pointer" }} aria-label="Close">×</button>
</div>
<div style={{ flex: 1 }}>
<CometChatConversations />
</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
// 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"}
=> 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
=> setSelected(undefined)}
style={{ alignSelf: "flex-start", background: "none", border: "none", pad
…(truncated)