Building a Messaging UI with Tabs, Sidebar, and Message View
This guide walks you through creating a tab-based messaging UI using React and CometChat UIKit. The UI will include different sections for Chats, Calls, Users, and Groups, allowing seamless navigation.
User Interface Preview

This layout consists of:
- Sidebar (Conversation List) – Displays recent conversations with active users and groups.
- Message View – Shows the selected chat with real-time messages.
- Message Input Box – Allows users to send messages seamlessly.
Step-by-Step Guide
Step 1: Create a Tab Component
To manage navigation, let's build a CometChatTabs
component. This component will render different tabs and allow switching between sections dynamically.
Folder Structure
Create a CometChatTabs
folder inside your src
directory and add the following files:
src/app
│── CometChatTabs/
│ ├── assets # These are the images you need to save
│ │ ├── chats.svg
│ │ ├── calls.svg
│ │ ├── users.svg
│ │ ├── groups.svg
│ ├── CometChatTabs.tsx
│ ├── CometChatTabs.css
Download the Icons
These icons are available in the CometChat UI Kit assets folder. You can find them at:
🔗 GitHub Assets Folder
Implementation
- TypeScript
- CSS
import { useState } from "react";
import "./CometChatTabs.css";
// Define icon paths for tabs
const chatsIcon = "/assets/chats.svg";
const callsIcon = "/assets/calls.svg";
const usersIcon = "/assets/users.svg";
const groupsIcon = "/assets/groups.svg";
// CometChatTabs component to display different tab options
export const CometChatTabs = (props: {
onTabClicked?: (tabItem: { name: string; icon?: string; }) => void; // Callback when a tab is clicked
activeTab?: string; // Currently active tab
}) => {
const {
onTabClicked = () => { }, // Default function if no prop is provided
activeTab
} = props;
// State to track the currently hovered tab
const [hoverTab, setHoverTab] = useState("");
// Array of tab items with their respective names and icons
const tabItems = [
{
"name": "CHATS",
"icon": chatsIcon // Icon for Chats tab
},
{
"name": "CALLS",
"icon": callsIcon // Icon for Calls tab
},
{
"name": "USERS",
"icon": usersIcon // Icon for Users tab
},
{
"name": "GROUPS",
"icon": groupsIcon // Icon for Groups tab
}
]
return (
<div className="cometchat-tab-component">
{
// Loop through tab items and render each tab
tabItems.map((tabItem) => (
<div
key={tabItem.name}
className="cometchat-tab-component__tab"
onClick={() => onTabClicked(tabItem)} // Handle tab click event
>
{/* Tab Icon */}
<div
className={(activeTab === tabItem.name.toLowerCase() || hoverTab === tabItem.name.toLowerCase())
? "cometchat-tab-component__tab-icon cometchat-tab-component__tab-icon-active"
: "cometchat-tab-component__tab-icon"}
style={{
WebkitMaskImage: `url(${tabItem.icon})`, // Apply mask for Webkit browsers
maskImage: `url(${tabItem.icon})` // Standard mask image
}}
onMouseEnter={() => setHoverTab(tabItem.name.toLowerCase())} // Track mouse hover
onMouseLeave={() => setHoverTab("")}
/>
{/* Tab Text */}
<div
className={(activeTab === tabItem.name.toLowerCase() || hoverTab === tabItem.name.toLowerCase())
? "cometchat-tab-component__tab-text cometchat-tab-component__tab-text-active"
: "cometchat-tab-component__tab-text"}
onMouseEnter={() => setHoverTab(tabItem.name.toLowerCase())}
onMouseLeave={() => setHoverTab("")}
>
{tabItem.name} {/* Display tab name */}
</div>
</div>
))
}
</div>
);
};
/* Container for the CometChat tab component */
.cometchat-tab-component {
display: flex;
width: 100%;
padding: 0px 8px;
align-items: flex-start;
gap: 8px;
border-top: 1px solid var(--cometchat-border-color-light, #F5F5F5); /* Light border on top */
border-right: 1px solid var(--cometchat-border-color-light, #F5F5F5); /* Light border on right */
background: var(--cometchat-background-color-01, #FFF); /* Background color */
}
/* Individual tab item within the tab component */
.cometchat-tab-component__tab {
display: flex;
padding: 12px 0px 10px 0px;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 4px; /* Spacing between icon and text */
flex: 1 0 0; /* Flexible layout for even spacing */
min-height: 48px; /* Ensures a minimum height */
}
/* Tab icon styling */
.cometchat-tab-component__tab-icon {
display: flex;
width: 32px;
height: 32px;
justify-content: center;
align-items: center;
background: var(--cometchat-icon-color-secondary); /* Default icon color */
-webkit-mask-size: contain;
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
mask-repeat: no-repeat;
cursor: pointer; /* Pointer cursor for interaction */
}
/* Tab text styling */
.cometchat-tab-component__tab-text {
color: var(--cometchat-text-color-secondary, #727272); /* Default text color */
text-align: center;
font: var(--cometchat-font-caption1-medium, 500 12px Roboto); /* Font settings */
cursor: pointer; /* Pointer cursor for interaction */
}
/* Active state for tab icon */
.cometchat-tab-component__tab-icon-active {
background: var(--cometchat-icon-color-highlight); /* Highlight color for active tab */
}
/* Active state for tab text */
.cometchat-tab-component__tab-text-active {
color: var(--cometchat-text-color-highlight); /* Highlight color for active tab text */
}
Step 2: 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
- TypeScript
- CSS
import { useEffect, useState } from "react";
import { Call, Conversation, Group, User } from "@cometchat/chat-sdk-javascript";
import {
CometChatCallLogs,
CometChatConversations,
CometChatGroups,
CometChatUIKit,
CometChatUIKitLoginListener,
CometChatUsers
} from "@cometchat/chat-uikit-react";
import { CometChatTabs } from "../CometChatTabs/CometChatTabs";
import { CometChat } from "@cometchat/chat-sdk-javascript";
// Define props for CometChatSelector component
interface SelectorProps {
onSelectorItemClicked?: (input: User | Group | Conversation | Call, type: string) => void;
}
// CometChatSelector component to handle different chat elements
export const CometChatSelector = (props: SelectorProps) => {
const {
onSelectorItemClicked = () => { }, // Default callback function
} = props;
// State to track the logged-in user
const [loggedInUser, setLoggedInUser] = useState<CometChat.User | null>();
// State to store the currently selected item (Conversation, User, Group, or Call)
const [activeItem, setActiveItem] = useState<
CometChat.Conversation | CometChat.User | CometChat.Group | CometChat.Call | undefined
>();
// State to track the active tab
const [activeTab, setActiveTab] = useState<string>("chats");
// Effect to retrieve the logged-in user on component mount
useEffect(() => {
let loggedInUsers = CometChatUIKitLoginListener.getLoggedInUser();
setLoggedInUser(loggedInUsers);
}, [CometChatUIKitLoginListener?.getLoggedInUser()]);
// Function to log out the user
const logOut = () => {
CometChatUIKit.logout()
.then(() => {
setLoggedInUser(null);
})
.catch((error) => {
console.log("error", error);
});
};
return (
<>
{loggedInUser && (
<>
{/* Render different components based on the active tab */}
{activeTab == "chats" ? (
<CometChatConversations
activeConversation={activeItem instanceof CometChat.Conversation ? activeItem : undefined}
onItemClick={(e) => {
setActiveItem(e);
onSelectorItemClicked(e, "updateSelectedItem");
}}
/>
) : activeTab == "calls" ? (
<CometChatCallLogs
activeCall={activeItem as Call}
onItemClick={(e: Call) => {
setActiveItem(e);
onSelectorItemClicked(e, "updateSelectedItemCall");
}}
/>
) : activeTab == "users" ? (
<CometChatUsers
activeUser={activeItem as User}
onItemClick={(e) => {
setActiveItem(e);
onSelectorItemClicked(e, "updateSelectedItemUser");
}}
/>
) : activeTab == "groups" ? (
<CometChatGroups
activeGroup={activeItem as Group}
onItemClick={(e) => {
setActiveItem(e);
onSelectorItemClicked(e, "updateSelectedItemGroup");
}}
/>
) : null}
</>
)}
{/* Render the tabs component for navigation */}
<CometChatTabs
activeTab={activeTab}
onTabClicked={(item) => {
setActiveTab(item.name.toLowerCase());
}}
/>
</>
);
};
/* Style for the header menu button icon in the conversation list */
.selector-wrapper .cometchat-conversations .cometchat-list__header-menu .cometchat-button__icon {
background: var(--cometchat-icon-color-primary);
}
/* Change background color of the header menu button 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 header 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 inside conversations */
.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);
}
/* Disable cursor interaction for specific 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 the logout icon in submenu with error color */
.cometchat-menu-list__sub-menu-list-item-icon-log-out {
background-color: var(--cometchat-error-color, #F44649);
}
/* Style for logout menu item text with error color */
.cometchat-menu-list__sub-menu-item-title-log-out {
color: var(--cometchat-error-color, #F44649);
}
/* Enable pointer interaction for submenu items inside chat menu */
.chat-menu .cometchat .cometchat-menu-list__sub-menu-item-title {
cursor: pointer;
}
/* Remove box-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 3: Render Experience
Now we will update the CometChatNoSSR.tsx
& CometChatNoSSR.css
files to import these new components as below,
- 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";
// Constants for CometChat configuration
const COMETCHAT_CONSTANTS = {
APP_ID: "",
REGION: "",
AUTH_KEY: "",
};
// Functional component for CometChatNoSSR
const CometChatNoSSR: React.FC = () => {
// State to store the logged-in user
const [user, setUser] = useState<CometChat.User | undefined>(undefined);
// State to store selected user or group
const [selectedUser, setSelectedUser] = useState<CometChat.User | undefined>(undefined);
const [selectedGroup, setSelectedGroup] = useState<CometChat.Group | undefined>(undefined);
useEffect(() => {
// Initialize UIKit settings
const UIKitSettings = new UIKitSettingsBuilder()
.setAppId(COMETCHAT_CONSTANTS.APP_ID)
.setRegion(COMETCHAT_CONSTANTS.REGION)
.setAuthKey(COMETCHAT_CONSTANTS.AUTH_KEY)
.subscribePresenceForAllUsers()
.build();
// Initialize CometChat UIKit
CometChatUIKit.init(UIKitSettings)
?.then(() => {
console.log("Initialization completed successfully");
// Check if user is already logged in
CometChatUIKit.getLoggedinUser().then((loggedInUser) => {
if (!loggedInUser) {
// Perform login if no user is logged in
CometChatUIKit.login("superhero1")
.then((user) => {
console.log("Login Successful", { user });
setUser(user);
})
.catch((error) => console.error("Login failed", error));
} else {
console.log("Already logged-in", { loggedInUser });
setUser(loggedInUser);
}
});
})
.catch((error) => console.error("Initialization failed", error));
}, []);
return user ? (
<div className="conversations-with-messages">
{/* Sidebar with conversation list */}
<div className="conversations-wrapper">
<CometChatSelector
onSelectorItemClicked={(activeItem) => {
let item = activeItem;
// Extract the conversation participant
if (activeItem instanceof CometChat.Conversation) {
item = activeItem.getConversationWith();
}
// Update states based on the type of selected item
if (item instanceof CometChat.User) {
setSelectedUser(item as CometChat.User);
setSelectedGroup(undefined);
} else if (item instanceof CometChat.Group) {
setSelectedUser(undefined);
setSelectedGroup(item as CometChat.Group);
} else {
setSelectedUser(undefined);
setSelectedGroup(undefined);
}
}}
/>
</div>
{/* Message view section */}
{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>
) : undefined;
};
export default CometChatNoSSR;
/* Layout for the main container that holds conversations and messages */
.conversations-with-messages {
display: flex;
height: 100%;
width: 100%;
}
/* Sidebar wrapper for conversation list */
.conversations-wrapper {
height: 100%;
width: 480px; /* Fixed width for conversation list */
overflow: hidden; /* Hide any overflowing content */
display: flex;
flex-direction: column;
height: inherit;
}
/* Hide overflow content inside the conversation component */
.conversations-wrapper > .cometchat {
overflow: hidden;
}
/* Message section layout */
.messages-wrapper {
width: calc(100% - 480px); /* Take remaining space */
height: 100%;
display: flex;
flex-direction: column;
}
/* 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 4: Disabling SSR for CometChatNoSSR.tsx
in Next.js
In this update, we will disable Server-Side Rendering (SSR) for CometChatNoSSR.tsx
while keeping the rest of the application’s SSR functionality intact. This ensures that the CometChat UI Kit components load only on the client-side, preventing SSR-related issues.
Disabling SSR in index.tsx
Modify your index.tsx
file to dynamically import the CometChatNoSSR.tsx
component with { ssr: false }
.
import { Inter } from "next/font/google";
import dynamic from "next/dynamic";
const inter = Inter({ subsets: ["latin"] });
// Dynamically import CometChat component with SSR disabled
const CometChatComponent = dynamic(() => import("./CometChatNoSSR"), {
ssr: false,
});
export default function Home() {
return <CometChatComponent />;
}
Why disable SSR?
- The CometChat UI Kit depends on browser APIs (window, document, WebSockets).
- Next.js pre-renders components on the server, which can cause errors with browser-specific features.
- By setting
{ ssr: false }
, we ensure that CometChatNoSSR.tsx only loads on the client.
Step 5: Import CometChat UI Kit CSS
Next, add the following import statement to global.css to ensure CometChat UI Kit styles are included.
@import url("../../node_modules/@cometchat/chat-uikit-react/dist/styles/css-variables.css");
Step 6: Run the project
npm start
Next Steps
Enhance the User Experience
- Advanced Customizations – Personalize the chat UI to align with your brand.