ngx-support-chat

December 29, 2025 ยท View on GitHub

A pure presentational Angular component library for customer support chat interfaces. Built with Angular 21 signals, OnPush change detection, and comprehensive theming support.

npm version License: MIT

Features

  • Pure Presentational Components - All business logic delegated to parent; components handle only UI
  • Signal-Based APIs - Modern Angular 21 signals for inputs, outputs, and state
  • OnPush Change Detection - Optimized performance across all components
  • Virtual Scrolling - Efficient rendering for large message lists via CDK
  • CSS Custom Properties - 70+ design tokens for complete theming control
  • Markdown Support - Optional ngx-markdown integration for rich text
  • Accessibility - WCAG compliant with screen reader support, keyboard navigation
  • Message Grouping - Automatic grouping by date and sender
  • Quick Replies - Interactive buttons for guided conversations
  • File Attachments - Image previews and file upload support
  • Typing Indicators - Real-time typing status display

Live Demo

View the Demo

Table of Contents


Installation

ng add ngx-support-chat

The schematic will:

  • Install required peer dependencies (@angular/cdk)
  • Optionally add ngx-markdown for markdown support
  • Add CSS tokens import to your global styles

Manual Installation

npm install ngx-support-chat @angular/cdk

Add CSS tokens to your global styles:

// styles.scss
@import 'ngx-support-chat/styles/tokens.css';

Quick Start

1. Configure Providers

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideChatConfig } from 'ngx-support-chat';

export const appConfig: ApplicationConfig = {
  providers: [
    provideChatConfig({
      dateFormat: 'MMM d, yyyy',
      timeFormat: 'HH:mm',
      dateSeparatorLabels: {
        today: 'Today',
        yesterday: 'Yesterday'
      }
    })
  ]
};

2. Import and Use the Component

// app.component.ts
import { Component, signal } from '@angular/core';
import {
  ChatContainerComponent,
  ChatMessage,
  MessageSendEvent
} from 'ngx-support-chat';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ChatContainerComponent],
  template: `
    <ngx-chat-container
      [messages]="messages()"
      [currentUserId]="currentUserId"
      [typingIndicator]="typingIndicator()"
      [quickReplies]="quickReplies()"
      [pendingAttachments]="attachments()"
      [(inputValue)]="inputValue"
      (messageSend)="onMessageSend($event)"
      (messageRetry)="onRetry($event)"
      (attachmentSelect)="onFilesSelected($event)"
      (attachmentRemove)="onAttachmentRemove($event)"
      (quickReplySubmit)="onQuickReply($event)"
      (imagePreview)="onImageClick($event)"
      (fileDownload)="onFileDownload($event)"
    >
      <div chatHeader>Support Chat</div>
      <div chatEmptyState>Start a conversation</div>
    </ngx-chat-container>
  `
})
export class AppComponent {
  messages = signal<ChatMessage[]>([]);
  typingIndicator = signal(null);
  quickReplies = signal(null);
  attachments = signal([]);
  inputValue = signal('');
  currentUserId = 'user-1';

  onMessageSend(event: MessageSendEvent) {
    // Handle message send
  }
}

Components

ChatContainerComponent

The main container that orchestrates the complete chat interface.

Selector: ngx-chat-container

Inputs

InputTypeRequiredDefaultDescription
messagesChatMessage[]Yes-Array of chat messages to display
currentUserIdstringYes-Current user's ID (determines message alignment)
typingIndicatorTypingIndicator | nullNonullShows who is currently typing
quickRepliesQuickReplySet | nullNonullInteractive quick reply options
pendingAttachmentsAttachment[]No[]Files pending upload
inputValuestringNo''Two-way bound input value
disabledbooleanNofalseDisables the input

Outputs

OutputTypeDescription
messageSendMessageSendEventUser sends a message
messageRetryChatMessageUser retries a failed message
attachmentSelectFile[]User selects files
attachmentRemoveAttachmentUser removes a pending attachment
quickReplySubmitQuickReplySubmitEventUser submits a quick reply
imagePreviewImageContentUser clicks an image
fileDownloadFileContentUser requests file download
scrollTopvoidUser scrolls to top (for pagination)

Content Projection

<ngx-chat-container>
  <div chatHeader>Custom Header Content</div>
  <div chatEmptyState>No messages yet</div>
</ngx-chat-container>

ChatHeaderComponent

Header container with content projection.

Selector: ngx-chat-header

<ngx-chat-header>
  <img [src]="agentAvatar" alt="Agent" />
  <span>Support Chat</span>
</ngx-chat-header>

ChatMessageAreaComponent

Scrollable message list with virtual scrolling support.

Selector: ngx-chat-message-area

Inputs

InputTypeRequiredDefaultDescription
messagesChatMessage[]Yes-Messages to display
currentUserIdstringYes-Current user's ID
showAvatarsbooleanNotrueShow sender avatars
showSenderNamesbooleanNotrueShow sender names
itemSizenumberNo80Virtual scroll item size (px)
autoScrollToBottombooleanNotrueAuto-scroll on new messages

ChatMessageComponent

Individual message bubble with support for text, image, file, and system messages.

Selector: ngx-chat-message

Inputs

InputTypeRequiredDefaultDescription
messageChatMessageYes-The message to display
isCurrentUserbooleanNofalseAligns message to right
showAvatarbooleanNotrueShow avatar
showSenderNamebooleanNotrueShow sender name
isFirstInGroupbooleanNotrueFull bubble radius
isLastInGroupbooleanNotrueShows timestamp/status

ChatInputComponent

Auto-resizing textarea for message composition.

Selector: ngx-chat-input

Inputs

InputTypeDefaultDescription
valuestring''Two-way bound value
placeholderstring'Type a message...'Placeholder text
disabledbooleanfalseDisabled state
maxHeightnumber120Max height before scrolling

Outputs

OutputDescription
sendEnter pressed (without Shift)

ChatQuickRepliesComponent

Interactive buttons for guided responses.

Selector: ngx-chat-quick-replies

Supports three types:

  • confirmation - Single button, immediate submit
  • single-choice - Radio-style, selection submits
  • multiple-choice - Checkboxes with Submit button
<ngx-chat-quick-replies
  [quickReplies]="quickReplySet"
  (quickReplySubmit)="onSubmit($event)"
/>

ChatTypingIndicatorComponent

Animated typing indicator bubble.

Selector: ngx-chat-typing-indicator

<ngx-chat-typing-indicator
  [typingIndicator]="{ userId: 'agent-1', userName: 'Support Agent' }"
  [showText]="true"
/>

ChatFooterComponent

Container for input, attachments, and action buttons.

Selector: ngx-chat-footer

<ngx-chat-footer
  [pendingAttachments]="attachments"
  [(inputValue)]="messageText"
  [hasContent]="hasContent()"
  (messageSend)="onSend()"
  (attachmentSelect)="onFiles($event)"
>
  <button chatFooterPrefix>Emoji</button>
  <button chatFooterActions>Voice</button>
</ngx-chat-footer>

ChatAttachmentPreviewComponent

Displays pending file attachments as chips.

Selector: ngx-chat-attachment-preview

<ngx-chat-attachment-preview
  [attachments]="pendingAttachments"
  (attachmentRemove)="onRemove($event)"
/>

ChatDateSeparatorComponent

Date divider showing "Today", "Yesterday", or formatted date.

Selector: ngx-chat-date-separator

<ngx-chat-date-separator [date]="messageDate" />

Configuration

ChatConfig Interface

interface ChatConfig {
  markdown: {
    enabled: boolean;      // Enable markdown support
    displayMode: boolean;  // Render markdown in messages
    inputMode: boolean;    // Allow markdown in input
  };
  dateFormat: string;      // e.g., 'MMMM d, yyyy'
  timeFormat: string;      // e.g., 'HH:mm'
  dateSeparatorLabels: {
    today: string;         // Label for today
    yesterday: string;     // Label for yesterday
  };
}

Provider Configuration

import { provideChatConfig } from 'ngx-support-chat';

export const appConfig: ApplicationConfig = {
  providers: [
    provideChatConfig({
      markdown: {
        enabled: true,
        displayMode: true,
        inputMode: false
      },
      dateFormat: 'dd/MM/yyyy',
      timeFormat: 'h:mm a',
      dateSeparatorLabels: {
        today: 'Aujourd\'hui',
        yesterday: 'Hier'
      }
    })
  ]
};

Markdown Support

To enable markdown rendering, install and configure ngx-markdown:

npm install ngx-markdown marked
// app.config.ts
import { provideMarkdown, MarkdownService } from 'ngx-markdown';
import { provideChatConfig, MARKDOWN_SERVICE } from 'ngx-support-chat';

export const appConfig: ApplicationConfig = {
  providers: [
    provideMarkdown(),
    { provide: MARKDOWN_SERVICE, useExisting: MarkdownService },
    provideChatConfig({
      markdown: { enabled: true, displayMode: true }
    })
  ]
};

Models & Interfaces

ChatMessage

interface ChatMessage {
  id: string;                    // Unique identifier
  type: MessageType;             // 'text' | 'image' | 'file' | 'system'
  senderId: string;              // Sender's ID
  senderName: string;            // Display name
  senderAvatar?: string;         // Avatar URL
  timestamp: Date;               // Message time
  status: MessageStatus;         // 'sending' | 'sent' | 'delivered' | 'read' | 'failed'
  content: MessageContent;       // Type-specific content
}

Message Content Types

// Text message
interface TextContent {
  text: string;
}

// Image message
interface ImageContent {
  thumbnailUrl: string;
  fullUrl: string;
  altText?: string;
  width?: number;
  height?: number;
}

// File message
interface FileContent {
  fileName: string;
  fileSize?: number;
  fileType: string;
  downloadUrl: string;
  icon?: string;
}

// System message
interface SystemContent {
  text: string;
}

Type Guards

import {
  isTextMessage,
  isImageMessage,
  isFileMessage,
  isSystemMessage
} from 'ngx-support-chat';

if (isTextMessage(message)) {
  console.log(message.content.text);
}

QuickReplySet

interface QuickReplySet {
  id: string;
  type: QuickReplyType;           // 'confirmation' | 'single-choice' | 'multiple-choice'
  prompt?: string;                // Optional prompt text
  options: QuickReplyOption[];
  submitted: boolean;
  selectedValues?: unknown[];
}

interface QuickReplyOption {
  value: unknown;
  label: string;
  disabled?: boolean;
}

TypingIndicator

interface TypingIndicator {
  userId: string;
  userName: string;
  avatar?: string;
}

Attachment

interface Attachment {
  id: string;
  file: File;
  previewUrl?: string;      // For image previews
  uploadProgress?: number;  // 0-100
}

Event Types

interface MessageSendEvent {
  text: string;
  attachments: Attachment[];
}

interface QuickReplySubmitEvent {
  type: QuickReplyType;
  value: unknown;
}

Theming

The library uses CSS custom properties (design tokens) for complete theming control. All tokens use the --ngx- prefix.

Quick Theme Customization

:root {
  /* Primary brand color */
  --ngx-bubble-user-bg: #7c3aed;
  --ngx-button-primary-bg: #7c3aed;
  --ngx-quick-reply-border: #7c3aed;

  /* Dark mode */
  --ngx-chat-bg: #1a1a1a;
  --ngx-chat-message-area-bg: #121212;
}

Available Tokens

Color Tokens

TokenDefaultDescription
--ngx-chat-bg#ffffffMain container background
--ngx-chat-header-bg#ffffffHeader background
--ngx-chat-footer-bg#ffffffFooter background
--ngx-chat-message-area-bg#f5f5f5Message area background
--ngx-bubble-user-bg#0066ccUser message bubble
--ngx-bubble-user-text#ffffffUser message text
--ngx-bubble-agent-bg#e8e8e8Agent message bubble
--ngx-bubble-agent-text#1a1a1aAgent message text
--ngx-bubble-system-bgtransparentSystem message background
--ngx-bubble-system-text#666666System message text
--ngx-input-bg#ffffffInput background
--ngx-input-border#ddddddInput border
--ngx-input-focus-border#0066ccInput focus border
--ngx-button-primary-bg#0066ccPrimary button background
--ngx-button-primary-text#ffffffPrimary button text
--ngx-status-sending#999999Sending status color
--ngx-status-sent#666666Sent status color
--ngx-status-delivered#0066ccDelivered status color
--ngx-status-read#00cc66Read status color
--ngx-status-failed#cc0000Failed status color
--ngx-quick-reply-bg#ffffffQuick reply background
--ngx-quick-reply-border#0066ccQuick reply border
--ngx-quick-reply-selected-bg#0066ccSelected quick reply
--ngx-typing-indicator-dot#666666Typing dots color
--ngx-link-color#0066ccLink color
--ngx-timestamp-text#999999Timestamp color

Spacing Tokens

TokenDefaultDescription
--ngx-spacing-xs4pxExtra small spacing
--ngx-spacing-sm8pxSmall spacing
--ngx-spacing-md16pxMedium spacing
--ngx-spacing-lg24pxLarge spacing
--ngx-spacing-xl32pxExtra large spacing
--ngx-message-gap8pxGap between messages
--ngx-bubble-padding12px 16pxMessage bubble padding
--ngx-header-padding16pxHeader padding
--ngx-footer-padding12px 16pxFooter padding

Border Radius Tokens

TokenDefaultDescription
--ngx-radius-sm4pxSmall radius
--ngx-radius-md12pxMedium radius
--ngx-radius-lg16pxLarge radius
--ngx-radius-full9999pxFull/pill radius
--ngx-bubble-radius16pxMessage bubble radius
--ngx-input-radius20pxInput field radius
--ngx-button-radius20pxButton radius
--ngx-avatar-radius50%Avatar radius

Typography Tokens

TokenDefaultDescription
--ngx-font-familysystem-ui, ...Font family
--ngx-font-size-xs0.75remTimestamps, metadata
--ngx-font-size-sm0.875remSecondary text
--ngx-font-size-md1remMessage text
--ngx-font-size-lg1.125remHeaders
--ngx-font-weight-normal400Normal weight
--ngx-font-weight-medium500Medium weight
--ngx-font-weight-bold600Bold weight
--ngx-line-height1.5Base line height

Dimension Tokens

TokenDefaultDescription
--ngx-avatar-size36pxAvatar size
--ngx-max-bubble-width70%Max message width
--ngx-input-min-height44pxInput minimum height
--ngx-input-max-height120pxInput maximum height
--ngx-button-size44pxButton size

Animation Tokens

TokenDefaultDescription
--ngx-transition-duration200msTransition speed
--ngx-transition-easingcubic-bezier(0.4, 0, 0.2, 1)Easing function
--ngx-typing-animation-duration1.4sTyping dots animation

Dark Mode

Apply dark mode using CSS media query or class:

// Automatic dark mode
@media (prefers-color-scheme: dark) {
  :root {
    --ngx-chat-bg: #1a1a1a;
    --ngx-chat-message-area-bg: #121212;
    --ngx-bubble-agent-bg: #2d2d2d;
    --ngx-bubble-agent-text: #e0e0e0;
    --ngx-input-bg: #2d2d2d;
    --ngx-input-text: #e0e0e0;
    --ngx-input-border: #404040;
    --ngx-separator-line: #404040;
  }
}

// Or with a class
.dark-theme {
  --ngx-chat-bg: #1a1a1a;
  // ... other dark tokens
}

SCSS Mixins

For SCSS projects, import the theme mixins:

@use 'ngx-support-chat/styles/theme-default' as chat;

:root {
  @include chat.ngx-chat-default-tokens;
}

@media (prefers-color-scheme: dark) {
  :root {
    @include chat.ngx-chat-dark-tokens;
  }
}

Pipes

SafeMarkdownPipe

Transforms markdown text to sanitized HTML. Returns an Observable for async pipe usage.

<span [innerHTML]="message.text | safeMarkdown | async"></span>

Requirements:

  • ngx-markdown installed
  • MARKDOWN_SERVICE provided
  • markdown.enabled and markdown.displayMode set to true

TimeAgoPipe

Displays relative time (e.g., "2 minutes ago").

{{ message.timestamp | timeAgo }}
<!-- Output: "Just now", "5 minutes ago", "2 hours ago" -->

FileSizePipe

Formats bytes to human-readable size.

{{ 1536 | fileSize }}     <!-- "1.5 KB" -->
{{ 1048576 | fileSize:0 }} <!-- "1 MB" -->
{{ 2500000 | fileSize:2 }} <!-- "2.38 MB" -->

Directives

AutoResizeDirective

Auto-resizing textarea that grows with content.

Selector: [ngxAutoResize]

<!-- Default max height (120px) -->
<textarea ngxAutoResize></textarea>

<!-- Custom max height -->
<textarea [ngxAutoResize]="200"></textarea>

AutoScrollDirective

Auto-scrolls to bottom when new items are added (if user was at bottom).

Selector: [ngxAutoScroll]

<div [ngxAutoScroll]="messages()" [ngxAutoScrollThreshold]="100">
  @for (message of messages(); track message.id) {
    <ngx-chat-message [message]="message" />
  }
</div>
InputTypeDefaultDescription
ngxAutoScrollunknown[]requiredArray to watch for changes
ngxAutoScrollThresholdnumber100Distance from bottom to consider "at bottom"

Utilities

Message Grouping

Group messages by date and sender for efficient display:

import {
  groupMessagesByDate,
  shouldGroupWithPrevious,
  getTotalMessageCount,
  flattenGroupedMessages,
  DEFAULT_GROUP_THRESHOLD_MS  // 5 minutes
} from 'ngx-support-chat';

// Group messages
const grouped = groupMessagesByDate(messages, currentUserId);

// Check if messages should be grouped
const shouldGroup = shouldGroupWithPrevious(currentMsg, prevMsg, 5 * 60 * 1000);

// Get total count
const count = getTotalMessageCount(grouped);

// Flatten back to array
const flat = flattenGroupedMessages(grouped);

Date Helpers

import {
  formatDate,
  formatTime,
  isToday,
  isYesterday,
  isSameDay,
  startOfDay,
  getTimeDifferenceMs,
  getRelativeTime
} from 'ngx-support-chat';

formatDate(new Date(), 'MMMM d, yyyy');  // "December 29, 2025"
formatTime(new Date(), 'HH:mm');          // "14:30"
isToday(date);                            // true/false
isYesterday(date);                        // true/false
getRelativeTime(date);                    // "5 minutes ago"

Supported format tokens:

  • Date: yyyy, MMMM, MMM, MM, M, dd, d, EEEE, EEE
  • Time: HH, H, hh, h, mm, m, ss, s, a

Accessibility

The library is built with accessibility in mind:

Screen Reader Support

  • Live Announcements - New messages announced via LiveAnnouncer
  • ARIA Labels - All interactive elements have appropriate labels
  • Role Attributes - Proper semantic roles (list, listitem, etc.)
import { ChatAnnouncerService } from 'ngx-support-chat';

// Inject the service for custom announcements
constructor(private announcer: ChatAnnouncerService) {}

// Announce a message
this.announcer.announceMessage(message);

// Announce typing
this.announcer.announceTyping(indicator);

// Announce quick reply selection
this.announcer.announceQuickReplySelection(option);

Keyboard Navigation

KeyAction
TabNavigate between interactive elements
EnterSend message / Select option
Shift+EnterNew line in input
Arrow Up/DownNavigate messages in message area
EscapeExit navigation mode

Focus Management

  • Focus returns to input after sending messages
  • Focus returns to input after quick reply submission
  • Skip links for efficient navigation

Advanced Usage

Building a Complete Chat Service

@Injectable({ providedIn: 'root' })
export class ChatService {
  private readonly messages = signal<ChatMessage[]>([]);
  private readonly typing = signal<TypingIndicator | null>(null);

  readonly messages$ = this.messages.asReadonly();
  readonly typing$ = this.typing.asReadonly();

  sendMessage(event: MessageSendEvent): void {
    const tempId = `temp-${Date.now()}`;

    // Add optimistic message
    const message: ChatMessage = {
      id: tempId,
      type: 'text',
      senderId: this.currentUserId,
      senderName: 'You',
      timestamp: new Date(),
      status: 'sending',
      content: { text: event.text }
    };

    this.messages.update(msgs => [...msgs, message]);

    // Send to API
    this.api.sendMessage(event).subscribe({
      next: (response) => {
        this.messages.update(msgs =>
          msgs.map(m => m.id === tempId
            ? { ...m, id: response.id, status: 'sent' }
            : m
          )
        );
      },
      error: () => {
        this.messages.update(msgs =>
          msgs.map(m => m.id === tempId
            ? { ...m, status: 'failed' }
            : m
          )
        );
      }
    });
  }

  setTyping(indicator: TypingIndicator | null): void {
    this.typing.set(indicator);
  }
}

Custom Message Types

Extend the library for custom content:

// Define custom content
interface CustomContent {
  type: 'custom';
  payload: Record<string, unknown>;
}

// Create message
const message: ChatMessage = {
  id: '1',
  type: 'text', // Use 'text' as base type
  senderId: 'system',
  senderName: 'System',
  timestamp: new Date(),
  status: 'sent',
  content: {
    text: JSON.stringify(customPayload) // Encode in text
  }
};

// Render custom content in your component
@if (isCustomMessage(message)) {
  <my-custom-renderer [data]="parseCustomContent(message)" />
}

WebSocket Integration

@Injectable({ providedIn: 'root' })
export class RealtimeChatService {
  private socket: WebSocket;

  connect(): void {
    this.socket = new WebSocket('wss://api.example.com/chat');

    this.socket.onmessage = (event) => {
      const data = JSON.parse(event.data);

      switch (data.type) {
        case 'message':
          this.addMessage(data.message);
          break;
        case 'typing':
          this.setTyping(data.indicator);
          break;
        case 'status':
          this.updateStatus(data.messageId, data.status);
          break;
      }
    };
  }
}

Browser Support

  • Chrome (latest)
  • Firefox (latest)
  • Safari (latest)
  • Edge (latest)

Requires Angular 21+.


Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/my-feature
  3. Commit changes: git commit -m 'Add my feature'
  4. Push to branch: git push origin feature/my-feature
  5. Open a Pull Request

License

MIT License - see LICENSE for details.