Skip to main content
This guide shows how to build a tab‑based messaging UI in Astro using the CometChat React UI Kit. The interface includes sections for Chats, Calls, Users, and Groups with a message panel.

User Interface Preview

Tabbed UI with chats, calls, users, and groups
Layout structure:
  1. Sidebar – conversations, users, groups, or call logs
  2. Messages – header, list, and composer
  3. Tabs – switch between Chats, Calls, Users, and Groups

Prerequisites

  • Astro project with React integration
  • CometChat credentials in .env
1

Create or open an Astro project

npm create astro@latest
cd <my-astro-app>
npm install
If you already have the sample astro-tab-based-chat project, open it instead.
2

Add React and install CometChat UI Kit

npx astro add react
npm i @cometchat/chat-uikit-react react react-dom
Add required environment variables to .env:
PUBLIC_COMETCHAT_APP_ID=your_app_id
PUBLIC_COMETCHAT_REGION=your_region
PUBLIC_COMETCHAT_AUTH_KEY=your_auth_key
# Login UID for tabbed example
PUBLIC_COMETCHAT_LOGIN_UID=cometchat-uid-3
Use Auth Tokens in production instead of Auth Keys.
3

Initialize CometChat (src/lib/cometchat-init.js)

Create src/lib/cometchat-init.js to initialize the UI Kit and provide a helper for login.
src/lib/cometchat-init.js
import { CometChatUIKit, UIKitSettingsBuilder } from "@cometchat/chat-uikit-react";

const APP_ID   = import.meta.env.PUBLIC_COMETCHAT_APP_ID;
const REGION   = import.meta.env.PUBLIC_COMETCHAT_REGION;
const AUTH_KEY = import.meta.env.PUBLIC_COMETCHAT_AUTH_KEY;

export async function initCometChat() {
  if (!APP_ID || !REGION || !AUTH_KEY) {
    throw new Error("Missing PUBLIC_COMETCHAT_* env vars.");
  }

  const settings = new UIKitSettingsBuilder()
    .setAppId(APP_ID)
    .setRegion(REGION)
    .setAuthKey(AUTH_KEY) // use Auth Tokens in prod
    .subscribePresenceForAllUsers()
    .build();

  await CometChatUIKit.init(settings);
}

export async function ensureLogin(uid) {
  const existing = await CometChatUIKit.getLoggedinUser();
  if (!existing) await CometChatUIKit.login(uid);
}
4

Create the Tabs component (src/components/CometChatTabs.jsx)

A simple bottom tab bar used to switch between sections.
src/components/CometChatTabs.jsx
import { useState } from "react";
// CSS styling is handled by tabs-layout.css imported in the main page

const chatsIcon  = "/assets/chats.svg";
const callsIcon  = "/assets/calls.svg";
const usersIcon  = "/assets/users.svg";
const groupsIcon = "/assets/groups.svg";

export default function CometChatTabs({ activeTab = "chats", onTabClicked = () => {} }) {
  const [hover, setHover] = useState("");

  const items = [
    { name: "CHATS",  icon: chatsIcon  },
    { name: "CALLS",  icon: callsIcon  },
    { name: "USERS",  icon: usersIcon  },
    { name: "GROUPS", icon: groupsIcon },
  ];

  return (
    <div className="cometchat-tab-component">
      {items.map((t) => {
        const key = t.name.toLowerCase();
        const active = activeTab === key || hover === key;
        return (
          <div
            key={t.name}
            className="cometchat-tab-component__tab"
            onClick={() => onTabClicked({ name: t.name })}
            onMouseEnter={() => setHover(key)}
            onMouseLeave={() => setHover("")}
          >
            {/* if icons not present, this still renders a label-only tab */}
            {t.icon ? (
              <div
                className={
                  "cometchat-tab-component__tab-icon " +
                  (active ? "cometchat-tab-component__tab-icon-active" : "")
                }
                style={{
                  WebkitMaskImage: `url(${t.icon})`,
                  maskImage: `url(${t.icon})`,
                }}
              />
            ) : null}
            <div
              className={
                "cometchat-tab-component__tab-text " +
                (active ? "cometchat-tab-component__tab-text-active" : "")
              }
            >
              {t.name}
            </div>
          </div>
        );
      })}
    </div>
  );
}
5

Build the React island (src/components/TabbedChat.jsx)

This component renders the sidebar list based on the active tab and shows the message panel on the right.
src/components/TabbedChat.jsx
import { useEffect, useState } from "react";
import {
  CometChatUIKit,
  CometChatConversations,
  CometChatUsers,
  CometChatGroups,
  CometChatCallLogs,
  CometChatMessageHeader,
  CometChatMessageList,
  CometChatMessageComposer,
} from "@cometchat/chat-uikit-react";
import "@cometchat/chat-uikit-react/css-variables.css";
import CometChatTabs from "./CometChatTabs.jsx";
import { initCometChat } from "../lib/cometchat-init.js";

const LOGIN_UID = import.meta.env.PUBLIC_COMETCHAT_LOGIN_UID; // UID of the user to log in as

export default function TabbedChat() {
  const [phase, setPhase] = useState("boot"); // boot | ready | error
  const [errorMsg, setErrorMsg] = useState("");

  const [activeTab, setActiveTab] = useState("chats"); // chats | calls | users | groups

  const [selectedUser, setSelectedUser]   = useState(null);
  const [selectedGroup, setSelectedGroup] = useState(null);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        // Use the centralized init function
        await initCometChat();

        // Check if current logged-in user matches the desired UID
        let me = await CometChatUIKit.getLoggedinUser();
        if (!me || me.getUid() !== LOGIN_UID) {
          // Logout current user if different UID, then login with correct UID
          if (me) {
            await CometChatUIKit.logout();
          }
          me = await CometChatUIKit.login(LOGIN_UID);
        }

        if (!cancelled) setPhase("ready");
      } catch (e) {
        console.error("TabbedChat init error:", e);
        if (!cancelled) {
          setErrorMsg(String(e?.message || e));
          setPhase("error");
        }
      }
    })();

    return () => { cancelled = true; };
  }, [LOGIN_UID]); // Add LOGIN_UID as dependency so effect runs when UID changes

  const handleSelect = (item) => {
    // item can be Conversation, User, Group, or Call
    const maybeConv = item?.getConversationWith ? item.getConversationWith() : null;
    const picked = maybeConv || item;

    if (picked?.getUid) {
      setSelectedUser(picked);
      setSelectedGroup(null);
      setActiveTab("chats"); // keep in chats context
    } else if (picked?.getGuid) {
      setSelectedGroup(picked);
      setSelectedUser(null);
      setActiveTab("chats"); // show messages in same area
    } else {
      // For calls tab, we don’t open message panel
      setSelectedUser(null);
      setSelectedGroup(null);
    }
  };

  if (phase === "boot")  return <div style={{ padding: 16 }}>Loading…</div>;
  if (phase === "error") return <div style={{ padding: 16, color: "crimson" }}><b>CometChat error:</b> {errorMsg}</div>;

  return (
    <div className="cc-tabbed">
      {/* LEFT: Sidebar */}
      <div className="cc-tabbed__sidebar">
        <div className="cc-tabbed__list">
          {activeTab === "chats" && (
            <CometChatConversations onItemClick={handleSelect} />
          )}

          {activeTab === "calls" && (
            <CometChatCallLogs
              onItemClick={(call) => {
                // If you integrate Calls SDK later, you can open call details here
                console.log("Call log clicked:", call);
              }}
            />
          )}

          {activeTab === "users" && (
            <CometChatUsers
              onItemClick={(user) => handleSelect(user)}
            />
          )}

          {activeTab === "groups" && (
            <CometChatGroups
              onItemClick={(group) => handleSelect(group)}
            />
          )}
        </div>

        {/* Tabs bar at bottom */}
        <CometChatTabs
          activeTab={activeTab}
          onTabClicked={(t) => setActiveTab(t.name.toLowerCase())}
        />
      </div>

      {/* RIGHT: Messages panel (appears for chats/users/groups) */}
      <div className="cc-tabbed__main">
        {activeTab === "calls" ? (
          <div className="cc-tabbed__empty">Select a call log</div>
        ) : selectedUser || selectedGroup ? (
          <>
            <CometChatMessageHeader user={selectedUser} group={selectedGroup} />
            <div className="cc-tabbed__list-slot">
              <CometChatMessageList user={selectedUser} group={selectedGroup} />
            </div>
            <CometChatMessageComposer user={selectedUser} group={selectedGroup} />
          </>
        ) : (
          <div className="cc-tabbed__empty">Select a conversation to start</div>
        )}
      </div>
    </div>
  );
}
6

Render the page (src/pages/index.astro)

Import the island and styles, then hydrate on the client.
src/pages/index.astro
---
import TabbedChat from "../components/TabbedChat.jsx";
import "../styles/tabs-layout.css";   // the CSS from this setup
// (optional) also import your existing globals.css if you have one
import "../styles/globals.css";
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Tabbed Messaging UI</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
  </head>
  <body>
    <!-- Client-only; CometChat needs browser APIs -->
    <TabbedChat client:only="react" />
  </body>
</html>
7

Run and verify

npm run dev
Log in using PUBLIC_COMETCHAT_LOGIN_UID, switch tabs, and open a conversation to send messages.

Troubleshooting

Ensure CometChatTabs is wired via onTabClicked and that the active tab state drives which list is rendered.
Verify .env contains PUBLIC_COMETCHAT_APP_ID, PUBLIC_COMETCHAT_REGION, PUBLIC_COMETCHAT_AUTH_KEY, and PUBLIC_COMETCHAT_LOGIN_UID.
The message panel shows only for Chats, Users, or Groups. Calls tab does not open a message panel.

Next Steps

  • Add call handling with CometChat Calls SDK
  • Apply theming and component overrides
  • Extend with unread badges and notifications
You can reuse src/lib/cometchat-init.js and swap the island component to build other experiences.