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.
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
Table of Contents
- Installation
- Quick Start
- Components
- Configuration
- Models & Interfaces
- Theming
- Pipes
- Directives
- Utilities
- Accessibility
- Advanced Usage
Installation
Using ng add (Recommended)
ng add ngx-support-chat
The schematic will:
- Install required peer dependencies (
@angular/cdk) - Optionally add
ngx-markdownfor 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
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
messages | ChatMessage[] | Yes | - | Array of chat messages to display |
currentUserId | string | Yes | - | Current user's ID (determines message alignment) |
typingIndicator | TypingIndicator | null | No | null | Shows who is currently typing |
quickReplies | QuickReplySet | null | No | null | Interactive quick reply options |
pendingAttachments | Attachment[] | No | [] | Files pending upload |
inputValue | string | No | '' | Two-way bound input value |
disabled | boolean | No | false | Disables the input |
Outputs
| Output | Type | Description |
|---|---|---|
messageSend | MessageSendEvent | User sends a message |
messageRetry | ChatMessage | User retries a failed message |
attachmentSelect | File[] | User selects files |
attachmentRemove | Attachment | User removes a pending attachment |
quickReplySubmit | QuickReplySubmitEvent | User submits a quick reply |
imagePreview | ImageContent | User clicks an image |
fileDownload | FileContent | User requests file download |
scrollTop | void | User 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
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
messages | ChatMessage[] | Yes | - | Messages to display |
currentUserId | string | Yes | - | Current user's ID |
showAvatars | boolean | No | true | Show sender avatars |
showSenderNames | boolean | No | true | Show sender names |
itemSize | number | No | 80 | Virtual scroll item size (px) |
autoScrollToBottom | boolean | No | true | Auto-scroll on new messages |
ChatMessageComponent
Individual message bubble with support for text, image, file, and system messages.
Selector: ngx-chat-message
Inputs
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
message | ChatMessage | Yes | - | The message to display |
isCurrentUser | boolean | No | false | Aligns message to right |
showAvatar | boolean | No | true | Show avatar |
showSenderName | boolean | No | true | Show sender name |
isFirstInGroup | boolean | No | true | Full bubble radius |
isLastInGroup | boolean | No | true | Shows timestamp/status |
ChatInputComponent
Auto-resizing textarea for message composition.
Selector: ngx-chat-input
Inputs
| Input | Type | Default | Description |
|---|---|---|---|
value | string | '' | Two-way bound value |
placeholder | string | 'Type a message...' | Placeholder text |
disabled | boolean | false | Disabled state |
maxHeight | number | 120 | Max height before scrolling |
Outputs
| Output | Description |
|---|---|
send | Enter 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
| Token | Default | Description |
|---|---|---|
--ngx-chat-bg | #ffffff | Main container background |
--ngx-chat-header-bg | #ffffff | Header background |
--ngx-chat-footer-bg | #ffffff | Footer background |
--ngx-chat-message-area-bg | #f5f5f5 | Message area background |
--ngx-bubble-user-bg | #0066cc | User message bubble |
--ngx-bubble-user-text | #ffffff | User message text |
--ngx-bubble-agent-bg | #e8e8e8 | Agent message bubble |
--ngx-bubble-agent-text | #1a1a1a | Agent message text |
--ngx-bubble-system-bg | transparent | System message background |
--ngx-bubble-system-text | #666666 | System message text |
--ngx-input-bg | #ffffff | Input background |
--ngx-input-border | #dddddd | Input border |
--ngx-input-focus-border | #0066cc | Input focus border |
--ngx-button-primary-bg | #0066cc | Primary button background |
--ngx-button-primary-text | #ffffff | Primary button text |
--ngx-status-sending | #999999 | Sending status color |
--ngx-status-sent | #666666 | Sent status color |
--ngx-status-delivered | #0066cc | Delivered status color |
--ngx-status-read | #00cc66 | Read status color |
--ngx-status-failed | #cc0000 | Failed status color |
--ngx-quick-reply-bg | #ffffff | Quick reply background |
--ngx-quick-reply-border | #0066cc | Quick reply border |
--ngx-quick-reply-selected-bg | #0066cc | Selected quick reply |
--ngx-typing-indicator-dot | #666666 | Typing dots color |
--ngx-link-color | #0066cc | Link color |
--ngx-timestamp-text | #999999 | Timestamp color |
Spacing Tokens
| Token | Default | Description |
|---|---|---|
--ngx-spacing-xs | 4px | Extra small spacing |
--ngx-spacing-sm | 8px | Small spacing |
--ngx-spacing-md | 16px | Medium spacing |
--ngx-spacing-lg | 24px | Large spacing |
--ngx-spacing-xl | 32px | Extra large spacing |
--ngx-message-gap | 8px | Gap between messages |
--ngx-bubble-padding | 12px 16px | Message bubble padding |
--ngx-header-padding | 16px | Header padding |
--ngx-footer-padding | 12px 16px | Footer padding |
Border Radius Tokens
| Token | Default | Description |
|---|---|---|
--ngx-radius-sm | 4px | Small radius |
--ngx-radius-md | 12px | Medium radius |
--ngx-radius-lg | 16px | Large radius |
--ngx-radius-full | 9999px | Full/pill radius |
--ngx-bubble-radius | 16px | Message bubble radius |
--ngx-input-radius | 20px | Input field radius |
--ngx-button-radius | 20px | Button radius |
--ngx-avatar-radius | 50% | Avatar radius |
Typography Tokens
| Token | Default | Description |
|---|---|---|
--ngx-font-family | system-ui, ... | Font family |
--ngx-font-size-xs | 0.75rem | Timestamps, metadata |
--ngx-font-size-sm | 0.875rem | Secondary text |
--ngx-font-size-md | 1rem | Message text |
--ngx-font-size-lg | 1.125rem | Headers |
--ngx-font-weight-normal | 400 | Normal weight |
--ngx-font-weight-medium | 500 | Medium weight |
--ngx-font-weight-bold | 600 | Bold weight |
--ngx-line-height | 1.5 | Base line height |
Dimension Tokens
| Token | Default | Description |
|---|---|---|
--ngx-avatar-size | 36px | Avatar size |
--ngx-max-bubble-width | 70% | Max message width |
--ngx-input-min-height | 44px | Input minimum height |
--ngx-input-max-height | 120px | Input maximum height |
--ngx-button-size | 44px | Button size |
Animation Tokens
| Token | Default | Description |
|---|---|---|
--ngx-transition-duration | 200ms | Transition speed |
--ngx-transition-easing | cubic-bezier(0.4, 0, 0.2, 1) | Easing function |
--ngx-typing-animation-duration | 1.4s | Typing 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-markdowninstalledMARKDOWN_SERVICEprovidedmarkdown.enabledandmarkdown.displayModeset totrue
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>
| Input | Type | Default | Description |
|---|---|---|---|
ngxAutoScroll | unknown[] | required | Array to watch for changes |
ngxAutoScrollThreshold | number | 100 | Distance 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
| Key | Action |
|---|---|
Tab | Navigate between interactive elements |
Enter | Send message / Select option |
Shift+Enter | New line in input |
Arrow Up/Down | Navigate messages in message area |
Escape | Exit 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
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Commit changes:
git commit -m 'Add my feature' - Push to branch:
git push origin feature/my-feature - Open a Pull Request
License
MIT License - see LICENSE for details.