Skip to main content
The CometChatMessageBubble component is a versatile container component that renders chat messages with configurable view slots. It serves as the primary wrapper for displaying messages in the CometChat Angular UIKit, handling message alignment, options menus, status indicators, and content rendering based on message type.

Overview

The Message Bubble component provides a flexible architecture for rendering different message types while maintaining consistent styling and behavior:
  • Message Type Routing: Automatically renders appropriate bubble component based on message type (text, image, video, audio, file)
  • Configurable View Slots: Override any section (leading, header, content, footer, etc.) with custom templates
  • Message Options Menu: Context menu with hover/click behavior for message actions
  • Group vs 1-on-1 Handling: Automatically shows/hides avatar and sender name based on conversation type
  • Alignment Variants: Supports incoming (left), outgoing (right), and action (center) alignments
  • Status Indicators: Displays timestamps, read receipts, and edited labels
  • Reply Preview: Shows quoted message preview for replies
  • Accessibility: Full keyboard navigation and screen reader support
Live Preview — Default message bubble preview. Open in Storybook ↗

Message Bubble Structure

The Message Bubble is composed of several configurable view slots that can be customized independently:
+----------------------------------------------------------------------------+
|                        Message Bubble Structure                            |
+----------------------------------------------------------------------------+
|                                                                            |
|  +----------+  +--------------------------------------------------------+  |
|  |          |  |  +--------------------------------------------------+  |  |
|  | Leading  |  |  |              Header View                         |  |  |
|  |  View    |  |  |           (Sender Name)                          |  |  |
|  | (Avatar) |  |  +--------------------------------------------------+  |  |
|  |          |  |  +--------------------------------------------------+  |  |
|  |          |  |  |              Reply View                          |  |  |
|  |          |  |  |           (Quoted Message)                       |  |  |
|  |          |  |  +--------------------------------------------------+  |  |
|  |          |  |  +--------------------------------------------------+  |  |
|  |          |  |  |              Content View                        |  |  |
|  |          |  |  |           (Message Content)                      |  |  |
|  |          |  |  |    Text / Image / Video / Audio / File           |  |  |
|  |          |  |  +--------------------------------------------------+  |  |
|  |          |  |  +--------------------------------------------------+  |  |
|  |          |  |  |           Status Info View                       |  |  |
|  |          |  |  |      (Timestamp, Receipts, Edited)               |  |  |
|  |          |  |  +--------------------------------------------------+  |  |
|  |          |  |  +--------------------------------------------------+  |  |
|  |          |  |  |              Bottom View                         |  |  |
|  |          |  |  |           (Link Preview, etc.)                   |  |  |
|  |          |  |  +--------------------------------------------------+  |  |
|  +----------+  +--------------------------------------------------------+  |
|                +----------------------------------------------------------+ |
|                |              Footer View                                 | |
|                |            (Reactions)                                   | |
|                +----------------------------------------------------------+ |
|                +----------------------------------------------------------+ |
|                |              Thread View                                 | |
|                |         (Reply Count, Thread Icon)                       | |
|                +----------------------------------------------------------+ |
|                                                                            |
+----------------------------------------------------------------------------+

View Slot Descriptions

View SlotDescriptionDefault Content
bubbleViewComplete bubble override - replaces entire message bubbleHeader + Reply + Content + StatusInfo + Bottom + Footer
leadingViewAvatar section on the left side of incoming messagesUser avatar with online status
headerViewSender information above the message contentSender name (shown in group chats)
replyViewQuoted message preview for reply messagesMessage preview component
contentViewMain message content areaText/Image/Video/Audio/File bubble
statusInfoViewTimestamp and delivery statusTime, receipts, edited label
bottomViewAdditional content below the messageLink previews, load more button
footerViewReactions and footer contentReaction emojis
threadViewThread reply indicatorsReply count and thread icon

Basic Usage

Simple Message Bubble

import { Component } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { CometChatMessageBubbleComponent, MessageBubbleAlignment } from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-message',
  standalone: true,
  imports: [CometChatMessageBubbleComponent],
  template: `
    <cometchat-message-bubble
      [message]="textMessage"
      [alignment]="MessageBubbleAlignment.left">
    </cometchat-message-bubble>
  `
})
export class MessageComponent {
  textMessage!: CometChat.TextMessage;
  MessageBubbleAlignment = MessageBubbleAlignment;
}

Incoming vs Outgoing Messages

import { Component } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { CometChatMessageBubbleComponent, MessageBubbleAlignment } from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-message-list',
  standalone: true,
  imports: [CometChatMessageBubbleComponent],
  template: `
    <!-- Incoming message (left-aligned) -->
    <cometchat-message-bubble
      [message]="incomingMessage"
      [alignment]="MessageBubbleAlignment.left">
    </cometchat-message-bubble>

    <!-- Outgoing message (right-aligned) -->
    <cometchat-message-bubble
      [message]="outgoingMessage"
      [alignment]="MessageBubbleAlignment.right">
    </cometchat-message-bubble>

    <!-- Action message (center-aligned) -->
    <cometchat-message-bubble
      [message]="actionMessage"
      [alignment]="MessageBubbleAlignment.center">
    </cometchat-message-bubble>
  `
})
export class MessageListComponent {
  incomingMessage!: CometChat.BaseMessage;
  outgoingMessage!: CometChat.BaseMessage;
  actionMessage!: CometChat.Action;
  MessageBubbleAlignment = MessageBubbleAlignment;
}

With Message Options

import { Component } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { 
  CometChatMessageBubbleComponent, 
  CometChatActionsIcon,
  MessageBubbleAlignment 
} from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-interactive-message',
  standalone: true,
  imports: [CometChatMessageBubbleComponent],
  template: `
    <cometchat-message-bubble
      [message]="message"
      [alignment]="MessageBubbleAlignment.left"
      [options]="messageOptions"
      (optionClick)="onOptionClick($event)">
    </cometchat-message-bubble>
  `
})
export class InteractiveMessageComponent {
  message!: CometChat.BaseMessage;
  MessageBubbleAlignment = MessageBubbleAlignment;

  messageOptions: CometChatActionsIcon[] = [
    new CometChatActionsIcon({ id: 'reply', title: 'Reply', iconURL: 'assets/reply.svg' }),
    new CometChatActionsIcon({ id: 'copy', title: 'Copy', iconURL: 'assets/copy.svg' }),
    new CometChatActionsIcon({ id: 'delete', title: 'Delete', iconURL: 'assets/delete.svg' }),
  ];

  onOptionClick(option: any): void {
    console.log('Option clicked:', option.id);
  }
}

API Reference

Properties

PropertyTypeDefaultDescription
messageCometChat.BaseMessagerequiredThe CometChat message object to render
alignmentMessageBubbleAlignmentrightAlignment of the bubble: left (incoming), right (outgoing), center (action)
groupCometChat.Group | nullnullGroup context for group conversations. Enables avatar and sender name display
optionsArray<CometChatActionsIcon | CometChatActionsView>[]Message options to display in context menu
quickOptionsCountnumber2Number of options to show directly on bubble before overflow
textFormattersCometChatTextFormatter[][]Text formatters for text message content
dateFormatCalendarObjectundefinedCustom date format for timestamp display
translatedTextstringundefinedTranslated text to display for text messages
reactionsRequestBuilderCometChat.ReactionsRequestBuilderundefinedCustom request builder for fetching reactions
isSelectedbooleanfalseWhether the message bubble is currently selected
ariaPosinsetnumberundefinedARIA position-in-set attribute for the message in the list
ariaSetsizenumberundefinedARIA set-size attribute for the total message count

View Override Properties

PropertyTypeDefaultDescription
bubbleViewTemplateRef<any>undefinedComplete bubble override - replaces entire message bubble
leadingViewTemplateRef<any>undefinedCustom template for leading view (avatar section)
headerViewTemplateRef<any>undefinedCustom template for header view (sender name section)
replyViewTemplateRef<any>undefinedCustom template for reply view (quoted message preview)
contentViewTemplateRef<any>undefinedCustom template for content view (main message content)
bottomViewTemplateRef<any>undefinedCustom template for bottom view
footerViewTemplateRef<any>undefinedCustom template for footer view (reactions)
statusInfoViewTemplateRef<any>undefinedCustom template for status info view (timestamp, receipts)
threadViewTemplateRef<any>undefinedCustom template for thread view

Display Control Properties

PropertyTypeDefaultDescription
hideAvatarbooleanfalseWhether to hide the avatar
hideSenderNamebooleanfalseWhether to hide the sender name
hideReceiptsbooleanfalseWhether to hide read receipts
hideTimestampbooleanfalseWhether to hide the timestamp
showErrorbooleanfalseWhether to show error state indicator
hideModerationViewbooleanfalseWhether to hide moderation status indicators
disableInteractionbooleanfalseWhen true, disables all interactive elements within the bubble (context menu, reactions, etc.)

Events

EventPayload TypeDescription
optionClickContextMenuItemEmitted when a message option is clicked
avatarClickCometChat.UserEmitted when the avatar is clicked
replyPreviewClickCometChat.BaseMessageEmitted when the reply preview is clicked
threadRepliesClickCometChat.BaseMessageEmitted when the thread replies view is clicked
reactionClick{ reaction: CometChat.ReactionCount, message: CometChat.BaseMessage }Emitted when a reaction is clicked
reactionListItemClick{ reaction: CometChat.Reaction, message: CometChat.BaseMessage }Emitted when a reaction list item (user) is clicked
mediaToggleCometChat.BaseMessageEmitted when Space key toggles media playback on audio/video bubbles
messageActionsOpenCometChat.BaseMessageEmitted when message actions are triggered via keyboard

Bubble Parts Customization

Each bubble part can be customized independently using Angular TemplateRef. The customization follows a priority system:
  1. Input template ref (passed directly to component) - Highest priority
  2. Service configured view (via MessageBubbleConfigService) - Medium priority
  3. Default rendering - Lowest priority

bubbleView

The bubbleView provides complete control over the entire message bubble. When set, it replaces all other views (header, content, footer, etc.). Use Case: When you need a completely custom message layout that doesn’t follow the standard structure.
import { Component, TemplateRef, ViewChild } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { 
  CometChatMessageBubbleComponent, 
  MessageBubbleAlignment 
} from '@cometchat/chat-uikit-angular';
import { DatePipe } from '@angular/common';

@Component({
  selector: 'app-custom-bubble',
  standalone: true,
  imports: [CometChatMessageBubbleComponent, DatePipe],
  template: `
    <cometchat-message-bubble
      [message]="message"
      [alignment]="MessageBubbleAlignment.left"
      [bubbleView]="customBubble">
    </cometchat-message-bubble>

    <ng-template #customBubble let-message>
      <div class="custom-bubble">
        <div class="custom-bubble__avatar">
          <img [src]="message.getSender()?.getAvatar()" alt="Avatar" />
        </div>
        <div class="custom-bubble__content">
          <span class="custom-bubble__sender">{{ message.getSender()?.getName() }}</span>
          <p class="custom-bubble__text">{{ getMessageText(message) }}</p>
          <span class="custom-bubble__time">{{ message.getSentAt() * 1000 | date:'shortTime' }}</span>
        </div>
      </div>
    </ng-template>
  `,
  styles: [`
    .custom-bubble {
      display: flex;
      gap: var(--cometchat-spacing-3);
      padding: var(--cometchat-spacing-3);
      background: var(--cometchat-background-color-02);
      border-radius: var(--cometchat-radius-3);
    }
    .custom-bubble__avatar img {
      width: 40px;
      height: 40px;
      border-radius: var(--cometchat-radius-max);
    }
    .custom-bubble__content {
      display: flex;
      flex-direction: column;
      gap: var(--cometchat-spacing-1);
    }
    .custom-bubble__sender {
      font: var(--cometchat-font-caption1-bold);
      color: var(--cometchat-text-color-primary);
    }
    .custom-bubble__text {
      font: var(--cometchat-font-body-regular);
      color: var(--cometchat-text-color-primary);
      margin: 0;
    }
    .custom-bubble__time {
      font: var(--cometchat-font-caption2-regular);
      color: var(--cometchat-text-color-tertiary);
    }
  `]
})
export class CustomBubbleComponent {
  message!: CometChat.BaseMessage;
  MessageBubbleAlignment = MessageBubbleAlignment;

  getMessageText(message: CometChat.BaseMessage): string {
    if (message instanceof CometChat.TextMessage) {
      return message.getText();
    }
    return '';
  }
}

leadingView

The leadingView customizes the avatar section that appears on the left side of incoming messages in group conversations. Use Case: Custom avatar styling, adding status badges, or showing additional user information.
import { Component, TemplateRef, ViewChild } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { 
  CometChatMessageBubbleComponent, 
  MessageBubbleAlignment 
} from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-custom-leading',
  standalone: true,
  imports: [CometChatMessageBubbleComponent],
  template: `
    <cometchat-message-bubble
      [message]="message"
      [alignment]="MessageBubbleAlignment.left"
      [group]="group"
      [leadingView]="customLeading"
      (avatarClick)="onAvatarClick($event)">
    </cometchat-message-bubble>

    <ng-template #customLeading let-message>
      <div class="custom-leading" 
           tabindex="0"
           role="button"
           [attr.aria-label]="'View profile of ' + message.getSender()?.getName()">
        <img 
          class="custom-leading__avatar"
          [src]="message.getSender()?.getAvatar() || 'assets/default-avatar.png'" 
          [alt]="message.getSender()?.getName()" />
        <span class="custom-leading__badge" 
              [class.custom-leading__badge--online]="message.getSender()?.getStatus() === 'online'">
        </span>
      </div>
    </ng-template>
  `,
  styles: [`
    .custom-leading {
      position: relative;
      cursor: pointer;
    }
    .custom-leading__avatar {
      width: 36px;
      height: 36px;
      border-radius: var(--cometchat-radius-max);
      border: 2px solid var(--cometchat-border-color-light);
    }
    .custom-leading__badge {
      position: absolute;
      bottom: 0;
      right: 0;
      width: 12px;
      height: 12px;
      border-radius: var(--cometchat-radius-max);
      background: var(--cometchat-neutral-color-400);
      border: 2px solid var(--cometchat-background-color-01);
    }
    .custom-leading__badge--online {
      background: var(--cometchat-success-color);
    }
  `]
})
export class CustomLeadingComponent {
  message!: CometChat.BaseMessage;
  group!: CometChat.Group;
  MessageBubbleAlignment = MessageBubbleAlignment;

  onAvatarClick(user: CometChat.User): void {
    console.log('Avatar clicked:', user.getName());
  }
}

headerView

The headerView customizes the sender name section that appears above the message content in group conversations. Use Case: Adding user roles, custom badges, or additional sender information.
import { Component, TemplateRef, ViewChild } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { 
  CometChatMessageBubbleComponent, 
  MessageBubbleAlignment 
} from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-custom-header',
  standalone: true,
  imports: [CometChatMessageBubbleComponent],
  template: `
    <cometchat-message-bubble
      [message]="message"
      [alignment]="MessageBubbleAlignment.left"
      [group]="group"
      [headerView]="customHeader">
    </cometchat-message-bubble>

    <ng-template #customHeader let-message>
      <div class="custom-header">
        <span class="custom-header__name">{{ message.getSender()?.getName() }}</span>
        @if (isAdmin(message.getSender())) {
          <span class="custom-header__badge">Admin</span>
        }
        <span class="custom-header__status">• In meeting</span>
      </div>
    </ng-template>
  `,
  styles: [`
    .custom-header {
      display: flex;
      align-items: center;
      gap: var(--cometchat-spacing-2);
      padding-bottom: var(--cometchat-spacing-1);
    }
    .custom-header__name {
      font: var(--cometchat-font-caption1-bold);
      color: var(--cometchat-text-color-primary);
    }
    .custom-header__badge {
      font: var(--cometchat-font-caption2-medium);
      color: var(--cometchat-static-white);
      background: var(--cometchat-primary-color);
      padding: 2px var(--cometchat-spacing-2);
      border-radius: var(--cometchat-radius-1);
    }
    .custom-header__status {
      font: var(--cometchat-font-caption2-regular);
      color: var(--cometchat-text-color-tertiary);
    }
  `]
})
export class CustomHeaderComponent {
  message!: CometChat.BaseMessage;
  group!: CometChat.Group;
  MessageBubbleAlignment = MessageBubbleAlignment;

  isAdmin(user: CometChat.User | undefined): boolean {
    // Check if user is admin - implement your logic
    return user?.getRole() === 'admin';
  }
}

replyView

The replyView customizes the quoted message preview that appears when a message is a reply to another message. Use Case: Custom reply preview styling, showing additional context, or different preview formats.
import { Component, TemplateRef, ViewChild } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { 
  CometChatMessageBubbleComponent, 
  MessageBubbleAlignment 
} from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-custom-reply',
  standalone: true,
  imports: [CometChatMessageBubbleComponent],
  template: `
    <cometchat-message-bubble
      [message]="replyMessage"
      [alignment]="MessageBubbleAlignment.left"
      [replyView]="customReply"
      (replyPreviewClick)="onReplyClick($event)">
    </cometchat-message-bubble>

    <ng-template #customReply let-message>
      @if (getQuotedMessage(message); as quotedMessage) {
        <div class="custom-reply"
             tabindex="0"
             role="button"
             aria-label="Jump to original message"
             (click)="onReplyClick(quotedMessage)"
             (keydown.enter)="onReplyClick(quotedMessage)"
             (keydown.space)="onReplyClick(quotedMessage)">
          <div class="custom-reply__indicator"></div>
          <div class="custom-reply__content">
            <span class="custom-reply__sender">{{ quotedMessage.getSender()?.getName() }}</span>
            <span class="custom-reply__text">{{ getPreviewText(quotedMessage) }}</span>
          </div>
        </div>
      }
    </ng-template>
  `,
  styles: [`
    .custom-reply {
      display: flex;
      gap: var(--cometchat-spacing-2);
      padding: var(--cometchat-spacing-2);
      background: var(--cometchat-background-color-03);
      border-radius: var(--cometchat-radius-2);
      margin-bottom: var(--cometchat-spacing-2);
      cursor: pointer;
    }
    .custom-reply:hover {
      background: var(--cometchat-background-color-04);
    }
    .custom-reply:focus {
      outline: 2px solid var(--cometchat-primary-color);
      outline-offset: 2px;
    }
    .custom-reply__indicator {
      width: 3px;
      background: var(--cometchat-primary-color);
      border-radius: var(--cometchat-radius-max);
    }
    .custom-reply__content {
      display: flex;
      flex-direction: column;
      gap: var(--cometchat-spacing-1);
      overflow: hidden;
    }
    .custom-reply__sender {
      font: var(--cometchat-font-caption1-bold);
      color: var(--cometchat-primary-color);
    }
    .custom-reply__text {
      font: var(--cometchat-font-caption1-regular);
      color: var(--cometchat-text-color-secondary);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
  `]
})
export class CustomReplyComponent {
  replyMessage!: CometChat.BaseMessage;
  MessageBubbleAlignment = MessageBubbleAlignment;

  getQuotedMessage(message: CometChat.BaseMessage): CometChat.BaseMessage | null {
    if (typeof (message as any).getQuotedMessage === 'function') {
      return (message as any).getQuotedMessage() || null;
    }
    return null;
  }

  getPreviewText(message: CometChat.BaseMessage): string {
    if (message instanceof CometChat.TextMessage) {
      return message.getText().substring(0, 50) + (message.getText().length > 50 ? '...' : '');
    }
    return `[${message.getType()}]`;
  }

  onReplyClick(message: CometChat.BaseMessage): void {
    console.log('Navigate to message:', message.getId());
  }
}

contentView

The contentView customizes the main message content area. This is where the actual message (text, image, video, etc.) is displayed. Use Case: Custom message rendering, adding interactive elements, or completely different content layouts.
import { Component, TemplateRef, ViewChild } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { 
  CometChatMessageBubbleComponent, 
  MessageBubbleAlignment 
} from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-custom-content',
  standalone: true,
  imports: [CometChatMessageBubbleComponent],
  template: `
    <cometchat-message-bubble
      [message]="message"
      [alignment]="MessageBubbleAlignment.left"
      [contentView]="customContent">
    </cometchat-message-bubble>

    <ng-template #customContent let-message>
      <div class="custom-content">
        @if (isTextMessage(message)) {
          <p class="custom-content__text">{{ message.getText() }}</p>
        } @else if (isImageMessage(message)) {
          <div class="custom-content__image-wrapper">
            <img 
              class="custom-content__image"
              [src]="getImageUrl(message)" 
              [alt]="'Image from ' + message.getSender()?.getName()"
              loading="lazy" />
          </div>
        } @else {
          <p class="custom-content__fallback">{{ message.getType() }} message</p>
        }
        <button class="custom-content__action" (click)="onCustomAction(message)">
          Custom Action
        </button>
      </div>
    </ng-template>
  `,
  styles: [`
    .custom-content {
      display: flex;
      flex-direction: column;
      gap: var(--cometchat-spacing-2);
    }
    .custom-content__text {
      font: var(--cometchat-font-body-regular);
      color: var(--cometchat-text-color-primary);
      margin: 0;
      word-wrap: break-word;
    }
    .custom-content__image-wrapper {
      max-width: 250px;
      border-radius: var(--cometchat-radius-2);
      overflow: hidden;
    }
    .custom-content__image {
      width: 100%;
      height: auto;
      display: block;
    }
    .custom-content__fallback {
      font: var(--cometchat-font-body-regular);
      color: var(--cometchat-text-color-secondary);
      font-style: italic;
    }
    .custom-content__action {
      font: var(--cometchat-font-button-medium);
      color: var(--cometchat-primary-color);
      background: transparent;
      border: 1px solid var(--cometchat-primary-color);
      border-radius: var(--cometchat-radius-2);
      padding: var(--cometchat-spacing-2) var(--cometchat-spacing-3);
      cursor: pointer;
    }
    .custom-content__action:hover {
      background: var(--cometchat-extended-primary-color-50);
    }
  `]
})
export class CustomContentComponent {
  message!: CometChat.BaseMessage;
  MessageBubbleAlignment = MessageBubbleAlignment;

  isTextMessage(message: CometChat.BaseMessage): message is CometChat.TextMessage {
    return message instanceof CometChat.TextMessage;
  }

  isImageMessage(message: CometChat.BaseMessage): message is CometChat.MediaMessage {
    return message instanceof CometChat.MediaMessage && message.getType() === 'image';
  }

  getImageUrl(message: CometChat.BaseMessage): string {
    if (message instanceof CometChat.MediaMessage) {
      return message.getAttachment()?.getUrl() || '';
    }
    return '';
  }

  onCustomAction(message: CometChat.BaseMessage): void {
    console.log('Custom action on message:', message.getId());
  }
}

statusInfoView

The statusInfoView customizes the timestamp and delivery status section that appears inside the message bubble. Use Case: Custom timestamp formats, additional status indicators, or different receipt icons.
import { Component, TemplateRef, ViewChild } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { 
  CometChatMessageBubbleComponent, 
  MessageBubbleAlignment 
} from '@cometchat/chat-uikit-angular';
import { DatePipe } from '@angular/common';

@Component({
  selector: 'app-custom-status',
  standalone: true,
  imports: [CometChatMessageBubbleComponent, DatePipe],
  template: `
    <cometchat-message-bubble
      [message]="message"
      [alignment]="MessageBubbleAlignment.right"
      [statusInfoView]="customStatus">
    </cometchat-message-bubble>

    <ng-template #customStatus let-message>
      <div class="custom-status" role="status" aria-live="polite">
        @if (message.getEditedAt()) {
          <span class="custom-status__edited">edited</span>
        }
        <span class="custom-status__time">
          {{ message.getSentAt() * 1000 | date:'shortTime' }}
        </span>
        @if (isOutgoing(message)) {
          <span class="custom-status__receipt" [attr.aria-label]="getReceiptLabel(message)">
            @if (message.getReadAt()) {
              <span class="custom-status__icon custom-status__icon--read">✓✓</span>
            } @else if (message.getDeliveredAt()) {
              <span class="custom-status__icon custom-status__icon--delivered">✓✓</span>
            } @else if (message.getSentAt()) {
              <span class="custom-status__icon custom-status__icon--sent">✓</span>
            } @else {
              <span class="custom-status__icon custom-status__icon--pending">○</span>
            }
          </span>
        }
      </div>
    </ng-template>
  `,
  styles: [`
    .custom-status {
      display: flex;
      align-items: center;
      gap: var(--cometchat-spacing-1);
      justify-content: flex-end;
    }
    .custom-status__edited {
      font: var(--cometchat-font-caption2-regular);
      color: var(--cometchat-text-color-tertiary);
      font-style: italic;
    }
    .custom-status__time {
      font: var(--cometchat-font-caption2-regular);
      color: var(--cometchat-text-color-tertiary);
    }
    .custom-status__icon {
      font-size: 12px;
    }
    .custom-status__icon--read {
      color: var(--cometchat-info-color);
    }
    .custom-status__icon--delivered {
      color: var(--cometchat-text-color-secondary);
    }
    .custom-status__icon--sent {
      color: var(--cometchat-text-color-tertiary);
    }
    .custom-status__icon--pending {
      color: var(--cometchat-text-color-tertiary);
    }
  `]
})
export class CustomStatusComponent {
  message!: CometChat.BaseMessage;
  MessageBubbleAlignment = MessageBubbleAlignment;
  private loggedInUser = CometChat.getLoggedinUser();

  isOutgoing(message: CometChat.BaseMessage): boolean {
    return message.getSender()?.getUid() === this.loggedInUser?.getUid();
  }

  getReceiptLabel(message: CometChat.BaseMessage): string {
    if (message.getReadAt()) return 'Read';
    if (message.getDeliveredAt()) return 'Delivered';
    if (message.getSentAt()) return 'Sent';
    return 'Sending';
  }
}

bottomView

The bottomView customizes the section below the main content, typically used for link previews, warnings, or additional information. Use Case: Link previews, content warnings, or expandable content sections.
import { Component, TemplateRef, ViewChild } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { 
  CometChatMessageBubbleComponent, 
  MessageBubbleAlignment 
} from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-custom-bottom',
  standalone: true,
  imports: [CometChatMessageBubbleComponent],
  template: `
    <cometchat-message-bubble
      [message]="message"
      [alignment]="MessageBubbleAlignment.left"
      [bottomView]="customBottom">
    </cometchat-message-bubble>

    <ng-template #customBottom let-message>
      @if (hasLinkPreview(message)) {
        <div class="custom-bottom">
          <a class="custom-bottom__link" 
             [href]="getLinkUrl(message)" 
             target="_blank"
             rel="noopener noreferrer">
            <div class="custom-bottom__preview">
              <img class="custom-bottom__image" 
                   [src]="getLinkImage(message)" 
                   alt="Link preview" />
              <div class="custom-bottom__info">
                <span class="custom-bottom__title">{{ getLinkTitle(message) }}</span>
                <span class="custom-bottom__domain">{{ getLinkDomain(message) }}</span>
              </div>
            </div>
          </a>
        </div>
      }
      @if (hasWarning(message)) {
        <div class="custom-bottom__warning">
          <span class="custom-bottom__warning-icon">⚠️</span>
          <span class="custom-bottom__warning-text">This message may contain sensitive content</span>
        </div>
      }
    </ng-template>
  `,
  styles: [`
    .custom-bottom {
      margin-top: var(--cometchat-spacing-2);
    }
    .custom-bottom__link {
      text-decoration: none;
      color: inherit;
    }
    .custom-bottom__preview {
      display: flex;
      gap: var(--cometchat-spacing-2);
      padding: var(--cometchat-spacing-2);
      background: var(--cometchat-background-color-03);
      border-radius: var(--cometchat-radius-2);
      border: 1px solid var(--cometchat-border-color-light);
    }
    .custom-bottom__preview:hover {
      background: var(--cometchat-background-color-04);
    }
    .custom-bottom__image {
      width: 60px;
      height: 60px;
      object-fit: cover;
      border-radius: var(--cometchat-radius-1);
    }
    .custom-bottom__info {
      display: flex;
      flex-direction: column;
      gap: var(--cometchat-spacing-1);
    }
    .custom-bottom__title {
      font: var(--cometchat-font-caption1-medium);
      color: var(--cometchat-text-color-primary);
    }
    .custom-bottom__domain {
      font: var(--cometchat-font-caption2-regular);
      color: var(--cometchat-text-color-tertiary);
    }
    .custom-bottom__warning {
      display: flex;
      align-items: center;
      gap: var(--cometchat-spacing-2);
      padding: var(--cometchat-spacing-2);
      background: var(--cometchat-warning-color);
      background: rgba(255, 171, 0, 0.1);
      border-radius: var(--cometchat-radius-2);
      margin-top: var(--cometchat-spacing-2);
    }
    .custom-bottom__warning-text {
      font: var(--cometchat-font-caption1-regular);
      color: var(--cometchat-warning-color);
    }
  `]
})
export class CustomBottomComponent {
  message!: CometChat.BaseMessage;
  MessageBubbleAlignment = MessageBubbleAlignment;

  hasLinkPreview(message: CometChat.BaseMessage): boolean {
    // Check if message has link preview metadata
    return false; // Implement your logic
  }

  hasWarning(message: CometChat.BaseMessage): boolean {
    // Check if message should show warning
    return false; // Implement your logic
  }

  getLinkUrl(message: CometChat.BaseMessage): string { return ''; }
  getLinkImage(message: CometChat.BaseMessage): string { return ''; }
  getLinkTitle(message: CometChat.BaseMessage): string { return ''; }
  getLinkDomain(message: CometChat.BaseMessage): string { return ''; }
}

footerView

The footerView customizes the reactions section that appears below the message content. Use Case: Custom reaction display, adding additional footer actions, or different reaction layouts.
import { Component, TemplateRef, ViewChild } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { 
  CometChatMessageBubbleComponent, 
  MessageBubbleAlignment 
} from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-custom-footer',
  standalone: true,
  imports: [CometChatMessageBubbleComponent],
  template: `
    <cometchat-message-bubble
      [message]="message"
      [alignment]="MessageBubbleAlignment.left"
      [footerView]="customFooter"
      (reactionClick)="onReactionClick($event)">
    </cometchat-message-bubble>

    <ng-template #customFooter let-message>
      @if (getReactions(message).length > 0) {
        <div class="custom-footer">
          <div class="custom-footer__reactions" role="group" aria-label="Message reactions">
            @for (reaction of getReactions(message); track reaction.getReaction()) {
              <button 
                class="custom-footer__reaction"
                [class.custom-footer__reaction--reacted]="hasUserReacted(reaction)"
                (click)="onReactionClick({ reaction, message })"
                [attr.aria-label]="reaction.getReaction() + ' reaction, ' + reaction.getCount() + ' users'"
                [attr.aria-pressed]="hasUserReacted(reaction)">
                <span class="custom-footer__emoji">{{ reaction.getReaction() }}</span>
                <span class="custom-footer__count">{{ reaction.getCount() }}</span>
              </button>
            }
          </div>
          <button class="custom-footer__add" aria-label="Add reaction">
            <span>+</span>
          </button>
        </div>
      }
    </ng-template>
  `,
  styles: [`
    .custom-footer {
      display: flex;
      align-items: center;
      gap: var(--cometchat-spacing-2);
      margin-top: var(--cometchat-spacing-2);
    }
    .custom-footer__reactions {
      display: flex;
      flex-wrap: wrap;
      gap: var(--cometchat-spacing-1);
    }
    .custom-footer__reaction {
      display: flex;
      align-items: center;
      gap: var(--cometchat-spacing-1);
      padding: var(--cometchat-spacing-1) var(--cometchat-spacing-2);
      background: var(--cometchat-background-color-02);
      border: 1px solid var(--cometchat-border-color-light);
      border-radius: var(--cometchat-radius-max);
      cursor: pointer;
      transition: all 0.2s ease;
    }
    .custom-footer__reaction:hover {
      background: var(--cometchat-background-color-03);
    }
    .custom-footer__reaction:focus {
      outline: 2px solid var(--cometchat-primary-color);
      outline-offset: 2px;
    }
    .custom-footer__reaction--reacted {
      background: var(--cometchat-extended-primary-color-50);
      border-color: var(--cometchat-primary-color);
    }
    .custom-footer__emoji {
      font-size: 14px;
    }
    .custom-footer__count {
      font: var(--cometchat-font-caption2-medium);
      color: var(--cometchat-text-color-secondary);
    }
    .custom-footer__add {
      width: 28px;
      height: 28px;
      display: flex;
      align-items: center;
      justify-content: center;
      background: var(--cometchat-background-color-02);
      border: 1px dashed var(--cometchat-border-color-default);
      border-radius: var(--cometchat-radius-max);
      cursor: pointer;
      font: var(--cometchat-font-body-medium);
      color: var(--cometchat-text-color-secondary);
    }
    .custom-footer__add:hover {
      background: var(--cometchat-background-color-03);
      border-style: solid;
    }
  `]
})
export class CustomFooterComponent {
  message!: CometChat.BaseMessage;
  MessageBubbleAlignment = MessageBubbleAlignment;
  private loggedInUser = CometChat.getLoggedinUser();

  getReactions(message: CometChat.BaseMessage): CometChat.ReactionCount[] {
    return message.getReactions() || [];
  }

  hasUserReacted(reaction: CometChat.ReactionCount): boolean {
    return reaction.getReactedByMe() || false;
  }

  onReactionClick(event: { reaction: CometChat.ReactionCount; message: CometChat.BaseMessage }): void {
    console.log('Reaction clicked:', event.reaction.getReaction());
  }
}

threadView

The threadView customizes the thread reply indicator that appears when a message has replies. Use Case: Custom thread indicators, showing reply previews, or different thread navigation styles.
import { Component, TemplateRef, ViewChild } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { 
  CometChatMessageBubbleComponent, 
  MessageBubbleAlignment 
} from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-custom-thread',
  standalone: true,
  imports: [CometChatMessageBubbleComponent],
  template: `
    <cometchat-message-bubble
      [message]="message"
      [alignment]="MessageBubbleAlignment.left"
      [threadView]="customThread"
      (threadRepliesClick)="onThreadClick($event)">
    </cometchat-message-bubble>

    <ng-template #customThread let-message>
      @if (message.getReplyCount() > 0) {
        <button 
          class="custom-thread"
          (click)="onThreadClick(message)"
          [attr.aria-label]="message.getReplyCount() + ' replies, click to view thread'">
          <span class="custom-thread__icon">💬</span>
          <span class="custom-thread__count">{{ message.getReplyCount() }}</span>
          <span class="custom-thread__label">
            {{ message.getReplyCount() === 1 ? 'reply' : 'replies' }}
          </span>
          @if (message.getUnreadRepliesCount() > 0) {
            <span class="custom-thread__unread">
              {{ message.getUnreadRepliesCount() }} new
            </span>
          }
          <span class="custom-thread__arrow">→</span>
        </button>
      }
    </ng-template>
  `,
  styles: [`
    .custom-thread {
      display: flex;
      align-items: center;
      gap: var(--cometchat-spacing-2);
      padding: var(--cometchat-spacing-2) var(--cometchat-spacing-3);
      background: transparent;
      border: none;
      cursor: pointer;
      margin-top: var(--cometchat-spacing-2);
      border-top: 1px solid var(--cometchat-border-color-light);
      width: 100%;
    }
    .custom-thread:hover {
      background: var(--cometchat-background-color-02);
    }
    .custom-thread:focus {
      outline: 2px solid var(--cometchat-primary-color);
      outline-offset: -2px;
    }
    .custom-thread__icon {
      font-size: 14px;
    }
    .custom-thread__count {
      font: var(--cometchat-font-caption1-bold);
      color: var(--cometchat-primary-color);
    }
    .custom-thread__label {
      font: var(--cometchat-font-caption1-regular);
      color: var(--cometchat-primary-color);
    }
    .custom-thread__unread {
      font: var(--cometchat-font-caption2-medium);
      color: var(--cometchat-static-white);
      background: var(--cometchat-primary-color);
      padding: 2px var(--cometchat-spacing-2);
      border-radius: var(--cometchat-radius-max);
    }
    .custom-thread__arrow {
      margin-left: auto;
      color: var(--cometchat-text-color-tertiary);
    }
  `]
})
export class CustomThreadComponent {
  message!: CometChat.BaseMessage;
  MessageBubbleAlignment = MessageBubbleAlignment;

  onThreadClick(message: CometChat.BaseMessage): void {
    console.log('Open thread for message:', message.getId());
  }
}

Service-Based Customization

For global customization across all message bubbles, use the MessageBubbleConfigService. This allows you to set custom views that apply to all instances without passing templates to each component.

Using MessageBubbleConfigService

import { Component, OnInit, TemplateRef, ViewChild, AfterViewInit } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { 
  CometChatMessageListComponent,
  MessageBubbleConfigService 
} from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-global-customization',
  standalone: true,
  imports: [CometChatMessageListComponent],
  template: `
    <cometchat-message-list [user]="user"></cometchat-message-list>

    <!-- Global custom status info view -->
    <ng-template #globalStatusInfo let-message>
      <div class="global-status">
        <span>{{ message.getSentAt() * 1000 | date:'shortTime' }}</span>
        @if (message.getEditedAt()) {
          <span class="global-status__edited">(edited)</span>
        }
      </div>
    </ng-template>

    <!-- Custom content view for text messages -->
    <ng-template #customTextContent let-message>
      <div class="custom-text">
        <p>{{ message.getText() }}</p>
      </div>
    </ng-template>
  `,
  styles: [`
    .global-status {
      display: flex;
      gap: var(--cometchat-spacing-1);
      font: var(--cometchat-font-caption2-regular);
      color: var(--cometchat-text-color-tertiary);
    }
    .global-status__edited {
      font-style: italic;
    }
    .custom-text p {
      margin: 0;
      font: var(--cometchat-font-body-regular);
      color: var(--cometchat-text-color-primary);
    }
  `]
})
export class GlobalCustomizationComponent implements AfterViewInit {
  @ViewChild('globalStatusInfo') globalStatusInfo!: TemplateRef<any>;
  @ViewChild('customTextContent') customTextContent!: TemplateRef<any>;
  
  user?: CometChat.User;

  constructor(private bubbleConfigService: MessageBubbleConfigService) {}

  ngAfterViewInit(): void {
    // Set global status info view for all message types
    this.bubbleConfigService.setGlobalStatusInfoView(this.globalStatusInfo);

    // Set custom content view for text messages only
    this.bubbleConfigService.setBubbleView('text_message', {
      contentView: this.customTextContent
    });

    // Set multiple message type customizations at once
    this.bubbleConfigService.setMessageTemplates({
      'text_message': { contentView: this.customTextContent },
      'image_message': { statusInfoView: this.globalStatusInfo }
    });
  }
}

MessageBubbleConfigService API

MethodParametersDescription
setBubbleViewmessageType: string, map: BubblePartMapSet custom views for a specific message type
setGlobalStatusInfoViewview: TemplateRef<any>Set status info view for all message types
setGlobalLeadingViewview: TemplateRef<any>Set leading view for all message types
setGlobalHeaderViewview: TemplateRef<any>Set header view for all message types
setMessageTemplatesmaps: Record<string, BubblePartMap>Set multiple message type customizations
getViewmessageType: string, part: BubblePartGet the configured view for a message type and part
clearAllnoneClear all configured views

Scoping for Multiple Instances

By default, MessageBubbleConfigService is a root-level singleton — all message bubbles share the same configuration. If you need different bubble customizations for different message lists (e.g., a main chat vs. a thread panel), create a wrapper component that provides its own instance:
@Component({
  selector: 'app-thread-panel',
  standalone: true,
  imports: [CometChatMessageListComponent],
  providers: [MessageBubbleConfigService], // Scoped instance
  template: `<cometchat-message-list [user]="user" [parentMessageId]="parentMessageId"></cometchat-message-list>`
})
export class ThreadPanelComponent implements AfterViewInit {
  @ViewChild('minimalStatus') minimalStatus!: TemplateRef<any>;

  // This is the LOCAL instance, not the root singleton
  private bubbleConfig = inject(MessageBubbleConfigService);

  ngAfterViewInit(): void {
    // Only affects bubbles inside this wrapper
    this.bubbleConfig.setGlobalView('statusInfoView', this.minimalStatus);
  }
}
See CometChatMessageList — Multiple Message Lists with Different Configurations for a complete example.

BubblePart Type

type BubblePart = 
  | 'bubbleView' 
  | 'contentView' 
  | 'bottomView' 
  | 'footerView' 
  | 'leadingView' 
  | 'headerView' 
  | 'statusInfoView' 
  | 'replyView'
  | 'threadView';

CSS Customization

Styling with CSS Variables

The Message Bubble component uses CSS variables for consistent theming. Override these variables to customize the appearance:
/* Custom theme overrides */
:root {
  /* Spacing */
  --cometchat-spacing-1: 4px;
  --cometchat-spacing-2: 8px;
  --cometchat-spacing-3: 12px;
  --cometchat-spacing-4: 16px;

  /* Typography */
  --cometchat-font-body-regular: 400 14px/1.4 'Roboto', sans-serif;
  --cometchat-font-caption1-medium: 500 12px/1.2 'Roboto', sans-serif;
  --cometchat-font-caption2-regular: 400 10px/1.2 'Roboto', sans-serif;

  /* Colors */
  --cometchat-background-color-02: #F5F5F5;
  --cometchat-primary-color: #6852D6;
  --cometchat-text-color-primary: #141414;
  --cometchat-text-color-secondary: #666666;
  --cometchat-text-color-tertiary: #999999;

  /* Border */
  --cometchat-border-color-light: #E0E0E0;
  --cometchat-radius-3: 12px;
}

Dark Theme

/* Dark theme overrides */
[data-theme="dark"] {
  --cometchat-background-color-01: #1A1A1A;
  --cometchat-background-color-02: #2C2C2C;
  --cometchat-background-color-03: #3D3D3D;
  --cometchat-text-color-primary: #FFFFFF;
  --cometchat-text-color-secondary: #B0B0B0;
  --cometchat-text-color-tertiary: #808080;
  --cometchat-border-color-light: #404040;
}

Component-Specific Styling

Target specific bubble elements using BEM class names:
/* Incoming message bubble */
.cometchat-message-bubble-incoming {
  /* Custom styles for incoming messages */
}

/* Outgoing message bubble */
.cometchat-message-bubble-outgoing {
  /* Custom styles for outgoing messages */
}

/* Action/system message bubble */
.cometchat-message-bubble-action {
  /* Custom styles for action messages */
}

/* Message type specific */
.cometchat-message-bubble__text-message {
  /* Custom styles for text messages */
}

.cometchat-message-bubble__image-message {
  /* Custom styles for image messages */
}

/* Bubble body */
.cometchat-message-bubble__body {
  /* Custom styles for bubble body */
}

/* Status info section */
.cometchat-message-bubble__status-info-view {
  /* Custom styles for status info */
}

/* Reply preview */
.cometchat-message-bubble__body-reply-view {
  /* Custom styles for reply preview */
}

Advanced Usage

Group Conversation with Avatar and Sender Name

import { Component } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { CometChatMessageBubbleComponent, MessageBubbleAlignment } from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-group-message',
  standalone: true,
  imports: [CometChatMessageBubbleComponent],
  template: `
    <cometchat-message-bubble
      [message]="message"
      [alignment]="MessageBubbleAlignment.left"
      [group]="currentGroup"
      (avatarClick)="onAvatarClick($event)">
    </cometchat-message-bubble>
  `
})
export class GroupMessageComponent {
  message!: CometChat.BaseMessage;
  currentGroup!: CometChat.Group;
  MessageBubbleAlignment = MessageBubbleAlignment;

  onAvatarClick(user: CometChat.User): void {
    console.log('Avatar clicked:', user.getName());
    // Navigate to user profile
  }
}

With Text Formatters

import { Component } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { 
  CometChatMessageBubbleComponent, 
  CometChatTextFormatter,
  MessageBubbleAlignment 
} from '@cometchat/chat-uikit-angular';

// Custom hashtag formatter
class HashtagFormatter extends CometChatTextFormatter {
  readonly id = 'hashtag-formatter';
  priority = 25;

  format(text: string, message: CometChat.BaseMessage): string {
    return text.replace(/#(\w+)/g, '<span class="hashtag">#$1</span>');
  }
}

@Component({
  selector: 'app-formatted-message',
  standalone: true,
  imports: [CometChatMessageBubbleComponent],
  template: `
    <cometchat-message-bubble
      [message]="message"
      [alignment]="MessageBubbleAlignment.left"
      [textFormatters]="formatters">
    </cometchat-message-bubble>
  `,
  styles: [`
    :host ::ng-deep .hashtag {
      color: var(--cometchat-primary-color);
      font-weight: 500;
      cursor: pointer;
    }
    :host ::ng-deep .hashtag:hover {
      text-decoration: underline;
    }
  `]
})
export class FormattedMessageComponent {
  message!: CometChat.TextMessage;
  MessageBubbleAlignment = MessageBubbleAlignment;
  formatters = [new HashtagFormatter()];
}

With Message Translation

import { Component } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { CometChatMessageBubbleComponent, MessageBubbleAlignment } from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-translated-message',
  standalone: true,
  imports: [CometChatMessageBubbleComponent],
  template: `
    <cometchat-message-bubble
      [message]="message"
      [alignment]="MessageBubbleAlignment.left"
      [translatedText]="translatedText">
    </cometchat-message-bubble>
  `
})
export class TranslatedMessageComponent {
  message!: CometChat.TextMessage;
  translatedText = 'This is the translated text';
  MessageBubbleAlignment = MessageBubbleAlignment;
}

Accessibility

The Message Bubble component is fully accessible and follows WCAG 2.1 Level AA guidelines:

Keyboard Support

KeyAction
TabNavigate between interactive elements
Enter / SpaceActivate focused element (avatar, options menu, reactions)
EscapeClose options menu and return focus to bubble
Arrow KeysNavigate within options menu when open

ARIA Attributes

AttributeElementPurpose
role="article"WrapperIdentifies message as an article
aria-labelWrapperDescribes message sender and type
aria-describedbyWrapperLinks to message content
role="status"Status infoIdentifies status area
aria-live="polite"Status infoAnnounces status changes
role="button"Interactive elementsIdentifies clickable elements
aria-pressedReactionsIndicates if user has reacted
role="group"Reactions containerGroups related reactions

Screen Reader Announcements

The component provides meaningful announcements for:
  • Message sender and type
  • Message status (sent, delivered, read)
  • Edited messages
  • Reactions and their counts
  • Thread reply counts

Best Practices

Use the group property to enable avatar and sender name display in group conversations.
Always provide the complete CometChat message object to ensure proper rendering of all message types.
The component automatically determines the appropriate bubble component based on message type. Use contentView only when you need custom rendering.
Handle optionClick events to implement message actions like reply, copy, delete, etc.
When using custom templates, ensure you maintain accessibility by including proper ARIA attributes and keyboard navigation.
Use CSS variables for styling to ensure your customizations work correctly with both light and dark themes.