Building a Conversation List + Message View
The Conversation List + Message View layout offers a seamless two-panel chat interface, commonly used in modern messaging applications like WhatsApp Web, Slack, and Microsoft Teams.
This design enables users to switch between conversations effortlessly while keeping the chat window open, ensuring a smooth, real-time messaging experience.
User Interface Overview

This layout is structured into three key sections:
- Sidebar (Conversation List) – Displays active conversations, including users and groups.
- Message View – Shows chat messages for the selected conversation in real-time.
- Message Composer – Provides an input field for typing and sending messages, along with support for media, emojis, and reactions.
Step-by-Step Guide
Step 1: Create Sidebar
Let's create the Sidebar
component which will render different conversations.
Folder Structure
Create a CometChatSelector
folder inside your src/app
directory and add the following files:
src/app/
│── CometChatSelector/
│ ├── CometChatSelector.tsx
│ ├── CometChatSelector.css
Download the Icon
These icons are available in the CometChat UI Kit assets folder. You can find them at:
🔗 GitHub Assets Folder
- TypeScript
- CSS
import { useEffect, useState } from "react";
import { Conversation, Group, User } from "@cometchat/chat-sdk-javascript";
import { CometChatConversations, CometChatUIKitLoginListener } from "@cometchat/chat-uikit-react";
import { CometChat } from '@cometchat/chat-sdk-javascript';
import "./CometChatSelector.css";
// Define props interface for component
interface SelectorProps {
onSelectorItemClicked?: (input: User | Group | Conversation, type: string) => void;
}
// CometChatSelector component definition
export const CometChatSelector = (props: SelectorProps) => {
const {
onSelectorItemClicked = () => { }, // Default function if no prop is provided
} = props;
// State to store the currently logged-in user
const [loggedInUser, setLoggedInUser] = useState<CometChat.User | null>();
// State to track the currently selected item (conversation, user, group, or call)
const [activeItem, setActiveItem] = useState<
CometChat.Conversation | CometChat.User | CometChat.Group | CometChat.Call | undefined
>();
useEffect(() => {
// Retrieve the logged-in user from CometChat's login listener
let loggedInUser = CometChatUIKitLoginListener.getLoggedInUser();
setLoggedInUser(loggedInUser);
}, [CometChatUIKitLoginListener?.getLoggedInUser()]); // Dependency array to trigger effect when user changes
return (
<>
{/* Render CometChatConversations only if a user is logged in */}
{loggedInUser && (
<>
<CometChatConversations
activeConversation={activeItem instanceof CometChat.Conversation ? activeItem : undefined}
onItemClick={(e) => {
setActiveItem(e); // Update the selected item state
onSelectorItemClicked(e, "updateSelectedItem"); // Trigger callback with selected item
}}
/>
</>
)}
</>
);
};
/* Style for the icon in the header menu of the conversation list */
.selector-wrapper .cometchat-conversations .cometchat-list__header-menu .cometchat-button__icon {
background: var(--cometchat-icon-color-primary);
}
/* Change background color of icon on hover */
.cometchat-conversations .cometchat-list__header-menu .cometchat-button__icon:hover {
background: var(--cometchat-icon-color-highlight);
}
/* Remove the right border from the search bar */
.cometchat-list__header-search-bar {
border-right: none;
}
/* Align submenu items to the left */
.cometchat .cometchat-menu-list__sub-menu-list-item {
text-align: left;
}
/* Set specific width and positioning for submenu list */
.cometchat .cometchat-conversations .cometchat-menu-list__sub-menu-list {
width: 212px;
top: 40px !important;
left: 172px !important;
}
/* Style the logged-in user section with a bottom border */
#logged-in-user {
border-bottom: 2px solid var(--cometchat-border-color-default, #E8E8E8);
}
/* Prevent cursor interaction on logged-in user menu items */
#logged-in-user .cometchat-menu-list__sub-menu-item-title,
#logged-in-user .cometchat-menu-list__sub-menu-list-item {
cursor: default;
}
/* Style for logout icon with error color */
.cometchat-menu-list__sub-menu-list-item-icon-log-out {
background-color: var(--cometchat-error-color, #F44649);
}
/* Style for logout text with error color */
.cometchat-menu-list__sub-menu-item-title-log-out {
color: var(--cometchat-error-color, #F44649);
}
/* Allow pointer interaction on submenu items inside chat menu */
.chat-menu .cometchat .cometchat-menu-list__sub-menu-item-title {
cursor: pointer;
}
/* Remove shadow from submenu inside chat menu */
.chat-menu .cometchat .cometchat-menu-list__sub-menu {
box-shadow: none;
}
/* Style for submenu icons inside chat menu */
.chat-menu .cometchat .cometchat-menu-list__sub-menu-icon {
background-color: var(--cometchat-icon-color-primary, #141414);
width: 24px;
height: 24px;
}
Step 2: Render Experience
Now we will create the CometChatNoSSR.tsx
& CometChatNoSSR.css
files.
Here, we will initialize the CometChat UI Kit, log in a user, and build the messaging experience using CometChatMessageHeader
, CometChatMessageList
, and CometChatMessageComposer
components.
src/app/
│── CometChatNoSSR/
│ ├── CometChatNoSSR.tsx
│ ├── CometChatNoSSR.css
- TypeScript
- CSS
import React, { useEffect, useState } from "react";
import {
CometChatMessageComposer,
CometChatMessageHeader,
CometChatMessageList,
CometChatUIKit,
UIKitSettingsBuilder
} from "@cometchat/chat-uikit-react";
import { CometChat } from "@cometchat/chat-sdk-javascript";
import { CometChatSelector } from "../CometChatSelector/CometChatSelector";
import "./CometChatNoSSR.css";
const COMETCHAT_CONSTANTS = {
APP_ID: "",
REGION: "",
AUTH_KEY: "",
};
const CometChatNoSSR: React.FC = () => {
const [initialized, setInitialized] = useState(false);
const [user, setUser] = useState<CometChat.User | null>(null);
const [selectedUser, setSelectedUser] = useState<CometChat.User>();
const [selectedGroup, setSelectedGroup] = useState<CometChat.Group>();
useEffect(() => {
if (typeof window === 'undefined') return;
const UIKitSettings = new UIKitSettingsBuilder()
.setAppId(COMETCHAT_CONSTANTS.APP_ID)
.setRegion(COMETCHAT_CONSTANTS.REGION)
.setAuthKey(COMETCHAT_CONSTANTS.AUTH_KEY)
.subscribePresenceForAllUsers()
.build();
CometChatUIKit.init(UIKitSettings)
?.then(() => {
console.log("Initialization completed successfully");
setInitialized(true);
CometChatUIKit.getLoggedinUser().then((loggedInUser) => {
if (!loggedInUser) {
CometChatUIKit.login("cometchat-uid-1")
.then((u) => {
console.log("Login Successful", { u });
setUser(u);
})
.catch((error) => console.error("Login failed", error));
} else {
console.log("Already logged-in", { loggedInUser });
setUser(loggedInUser);
}
});
})
.catch((error) => console.error("Initialization failed", error));
}, []);
if (!initialized || !user) {
return <div>Initializing Chat...</div>;
}
return (
<div className="conversations-with-messages">
<div className="conversations-wrapper">
<CometChatSelector
onSelectorItemClicked={(activeItem) => {
let item = activeItem;
if (activeItem instanceof CometChat.Conversation) {
item = activeItem.getConversationWith();
}
if (item instanceof CometChat.User) {
setSelectedUser(item);
setSelectedGroup(undefined);
} else if (item instanceof CometChat.Group) {
setSelectedUser(undefined);
setSelectedGroup(item);
} else {
setSelectedUser(undefined);
setSelectedGroup(undefined);
}
}}
/>
</div>
{selectedUser || selectedGroup ? (
<div className="messages-wrapper">
<CometChatMessageHeader user={selectedUser} group={selectedGroup} />
<CometChatMessageList user={selectedUser} group={selectedGroup} />
<CometChatMessageComposer user={selectedUser} group={selectedGroup} />
</div>
) : (
<div className="empty-conversation">Select Conversation to start</div>
)}
</div>
);
};
export default CometChatNoSSR;
/* Layout for the main conversations and messages container */
.conversations-with-messages {
display: flex;
height: 100%;
width: 100%;
}
/* Sidebar wrapper for conversations */
.conversations-wrapper {
height: 100%;
width: 480px; /* Fixed width for conversation list */
overflow: hidden;
display: flex;
flex-direction: column;
height: inherit;
}
/* Hide overflow for the conversation list */
.conversations-wrapper > .cometchat {
overflow: hidden;
}
/* Message section layout */
.messages-wrapper {
width: calc(100% - 480px); /* Take remaining space */
height: 100%;
display: flex;
flex-direction: column;
}
/* Display styling for when no conversation is selected */
.empty-conversation {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
background: white;
color: var(--cometchat-text-color-secondary, #727272);
font: var(--cometchat-font-body-regular, 400 14px Roboto);
}
/* Ensure message composer does not have rounded corners */
.cometchat .cometchat-message-composer {
border-radius: 0px;
}
Step 3: Disable SSR and Render the CometChat Component
Create a file CometChat.tsx inside the routes folder:
import React, { lazy, Suspense, useEffect, useState } from "react";
import "@cometchat/chat-uikit-react/css-variables.css";
// Lazy import to prevent SSR crash
const CometChatNoSSR = lazy(() => import("../CometChatNoSSR/CometChatNoSSR"));
export default function CometChatRoute() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return mounted ? (
<Suspense fallback={<div>Loading...</div>}>
<CometChatNoSSR />
</Suspense>
) : (
<div>Loading...</div>
);
}
Now, create a route for CometChat in your routes file:
import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/home.tsx"),
route("chat", "routes/CometChat.tsx"), // Chat Route
] satisfies RouteConfig;
Why disable SSR? CometChat Visual Builder relies on browser APIs such as window, document, and WebSockets. Since React Router renders on the server by default, disabling SSR for this component prevents runtime errors.
Step 4: Update App CSS
Next, add the following styles to app.css to ensure CometChat UI Kit is properly styled.
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
/** Give your App a height of `100%`. Keep other CSS properties in the below selector as it is. */
.root {
height: 100%;
}
html,
body {
height: 100%;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}
Step 5: Run Your Application
-
Start the development server
npm run dev
-
Verify the chat interface
- In your browser, navigate to the
/chat
route(e.g., http://localhost:3000/chat)
. - Confirm that the chat experience loads as expected.
Next Steps
Enhance the User Experience
- Advanced Customizations – Personalize the chat UI to align with your brand.