Skip to main content

Chat UI Design System Architecture

Comprehensive low-level design for scalable, real-time chat user interface with message handling and UI state management

This document outlines the low-level technical design for a production-ready chat user interface system, covering component structure, state management patterns, and real-time communication handling for scalable messaging applications.

1. Problem Statement & Requirements

Clarifying questions

  1. What target devices/browsers should we support? (e.g., mobile-first, desktop-only, IE11 support)
  2. Do we need authentication/authorization for chat access? What auth mechanism (JWT, OAuth, etc.)?
  3. Should chat messages be real-time or static (polling-based)? What's the expected latency for message delivery?
  4. Do we need offline support or should all chat operations require an active network connection?
  5. What level of accessibility compliance is required? (e.g., WCAG 2.1 AA, Section 508)
  6. Do we need internationalization support for multiple languages and RTL layouts?
  7. Are there existing design systems or component libraries we must integrate with?

Functional requirements

  • Users can view a list of conversations/chats
  • Users can open and read messages in a conversation
  • Users can send new messages to a conversation
  • Users can view message status (sent, delivered, read)
  • Users can see typing indicators from other participants
  • Users can view message timestamps
  • Users can search through conversations or messages
  • Users can mark conversations as read/unread
  • Users can view user avatars and names
  • Users can see message reactions (like, emoji)
  • Users can delete or archive conversations

Non-functional requirements

  • Performance: Messages should load within 200ms, typing indicators should update within 100ms
  • Accessibility: Full keyboard navigation support, ARIA roles for screen readers, WCAG 2.1 AA compliance
  • Scalability: Support for thousands of concurrent users and conversations
  • Reliability: Message persistence and delivery guarantees, graceful degradation during network issues
  • Security: End-to-end encryption support, XSS prevention, input sanitization

Constraints and assumptions

  • Backend API is available with defined endpoints for chat operations
  • Users have pre-existing authentication state (JWT or session tokens)
  • Messages are stored in a database with proper indexing for search and retrieval
  • UI should be responsive across mobile, tablet, and desktop form factors
  • No third-party chat services (e.g., Twilio, Firebase) are used - all logic is custom-built

Out of scope

  • Voice and video calling features
  • File attachment and media sharing capabilities
  • Push notifications for new messages
  • Chat moderation tools (banning, blocking users)
  • Group chat management features (adding/removing members)
  • Message encryption implementation details
  • Chat analytics dashboard
  • Admin panel for managing users and conversations

2. Component Architecture

graph TD
    A[ChatApp] --> B[ChatList]
    A --> C[ChatWindow]
    A --> D[ChatHeader]
    A --> E[ChatInput]
    B --> F[ChatItem]
    C --> G[MessageList]
    C --> H[Message]
    G --> I[MessageBubble]
    E --> J[AttachmentButton]
    E --> K[TextInput]
    E --> L[SendButton]
    
    style A fill:#f0f8ff,stroke:#333
    style B fill:#fff8dc,stroke:#333
    style C fill:#fff8dc,stroke:#333
    style D fill:#fff8dc,stroke:#333
    style E fill:#fff8dc,stroke:#333
    style F fill:#f0fff0,stroke:#333
    style G fill:#f0fff0,stroke:#333
    style H fill:#f0fff0,stroke:#333
    style I fill:#f0fff0,stroke:#333
    style J fill:#ffe4b5,stroke:#333
    style K fill:#ffe4b5,stroke:#333
    style L fill:#ffe4b5,stroke:#333

Component Tree Analysis

Smart Components (Container Components):

  • ChatApp - Main application container managing global state and routing
  • ChatList - Fetches and manages chat list state, handles selection logic
  • ChatWindow - Manages conversation state, handles message sending/receiving

Dumb Components (Presentational Components):

  • ChatItem - Renders individual chat list items
  • MessageList - Renders collection of messages
  • MessageBubble - Renders individual message bubbles with appropriate styling
  • TextInput - Simple input field component
  • SendButton - Action button for sending messages

TypeScript Props Contracts

// ChatApp Props
interface ChatAppProps {
  userId: string;
  initialChatId?: string;
}

// ChatList Props
interface ChatListProps {
  chats: ChatItem[];
  selectedChatId: string | null;
  onSelectChat: (chatId: string) => void;
  onNewChat: () => void;
}

// ChatItem Props
interface ChatItemProps {
  chat: ChatItem;
  isSelected: boolean;
  onClick: () => void;
}

// MessageList Props
interface MessageListProps {
  messages: Message[];
  currentUser: User;
  isLoading?: boolean;
  hasMore?: boolean;
  onLoadMore?: () => void;
}

// Message Props
interface MessageProps {
  message: Message;
  isCurrentUser: boolean;
  isLastOfBatch?: boolean;
}

// ChatInput Props
interface ChatInputProps {
  value: string;
  onChange: (value: string) => void;
  onSend: () => void;
  onAttachmentClick?: () => void;
  isSending?: boolean;
  disabled?: boolean;
}

UI State Machine

stateDiagram-v2
    [*] --> Idle
    Idle --> Composing : User types
    Composing --> Sending : User sends
    Sending --> Idle : Success
    Sending --> Error : Failure
    Error --> Composing : User retries
    Sending --> Idle : Timeout
    
    state Sending {
        [*] --> Preparing
        Preparing --> Requesting : Send to API
        Requesting --> Waiting : API response
        Waiting --> Success : 200 OK
        Waiting --> Failure : Error
        Failure --> [*] : Retry or Cancel
        Success --> [*]
    }

Component Responsibilities

ChatApp: Main application container that orchestrates routing, authentication, and global state management.

ChatList:

  • Displays list of conversations
  • Handles chat selection
  • Manages loading states and error handling for chat list fetch

ChatWindow:

  • Renders the conversation view
  • Manages message lifecycle (fetching, sending, receiving)
  • Handles real-time updates and typing indicators

ChatHeader:

  • Displays conversation metadata (participants, status)
  • Handles chat actions (leave, mute, etc.)

ChatInput:

  • Manages input composition and submission
  • Handles attachment actions
  • Implements send/retry logic with proper loading states

Reusability Decisions

  1. MessageBubble is highly reusable across different chat contexts (group chats, one-on-one, etc.)
  2. TextInput and SendButton are generic enough to be reused in other messaging contexts
  3. ChatItem is specific to chat list rendering but can be extended for different chat types
  4. AttachmentButton is generic and could be reused in other forms

Component Lifecycle Considerations

  1. ChatApp: Mounts, initializes auth, sets up real-time listeners, fetches initial data
  2. ChatList: Fetches chat list on mount, handles real-time chat updates, manages selection
  3. ChatWindow: Fetches messages on mount, sets up real-time message listeners, handles cleanup
  4. MessageList: Handles infinite scroll, manages loading states, implements virtualization
  5. ChatInput: Manages input state, handles keyboard shortcuts, implements auto-save for drafts

The component architecture prioritizes:

  • Clear separation of concerns between chat list and conversation view
  • Reusable UI elements that can be composed in different contexts
  • Proper state management with clear component responsibilities
  • Scalable structure that can handle multiple chat types and features

3. State Management Design

Local state vs. global state decisions

For a chat UI, we need to carefully distinguish between local and global state:

  • Local state: Message composition, input field content, UI visibility toggles (e.g., emoji picker, attachment dropdown)
  • Global state: Chat threads, user presence status, unread message counts, active chat session, user preferences

The global state is essential for maintaining consistency across multiple chat components and enabling features like push notifications or real-time updates.

State management approach

I'll use Zustand as the primary state management solution because:

  1. It provides a clean, intuitive API that's easy to understand and maintain
  2. It supports both local and global state with minimal boilerplate
  3. It has excellent TypeScript support with type inference
  4. It's lightweight and doesn't require complex setup like Redux Toolkit

Zustand is ideal for this use case because it allows us to create a single source of truth for chat data while keeping the component layer simple and testable.

State shape and normalization strategy

// Normalized state structure for chat UI
interface User {
  id: string;
  name: string;
  avatarUrl?: string;
  status: 'online' | 'offline' | 'away';
  lastSeen?: Date;
}

interface Message {
  id: string;
  content: string;
  senderId: string;
  timestamp: Date;
  status: 'sent' | 'delivered' | 'read';
  reactions?: Reaction[];
}

interface Reaction {
  emoji: string;
  count: number;
  userIds: string[];
}

interface ChatThread {
  id: string;
  participants: User[];
  messages: Message[];
  unreadCount: number;
  lastMessage?: Message;
  isPinned: boolean;
  isActive: boolean;
}

interface ChatState {
  currentUser: User | null;
  threads: Record<string, ChatThread>;
  activeThreadId: string | null;
  isComposing: boolean;
  typingUsers: Record<string, User[]>;
  unreadCounts: Record<string, number>;
  preferences: {
    autoScroll: boolean;
    notificationSound: boolean;
    theme: 'light' | 'dark';
  };
}

Server state vs. client state separation

  • Server state: Thread data, message history, user presence status
  • Client state: UI state (composition, active thread), optimistic updates, local preferences

Server state is managed through React Query for caching and synchronization, while client state is handled via Zustand for immediate UI updates.

Cache invalidation strategy

  1. Message sent: Optimistically update local state, then sync with server
  2. Thread updated: Invalidate thread cache and refetch if needed
  3. User status change: Update user presence in real-time without cache invalidation
  4. New message received: Add to local thread and update unread counts

Optimistic updates design

When a user sends a message:

  1. Immediately add to local thread state with "sent" status
  2. Show loading indicator in UI
  3. Dispatch to server API
  4. If successful, update status to "delivered"
  5. If failed, rollback to local state with error indicator

Critical-path hook implementation

// src/features/chat/hooks/useChatStore.ts
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import type { Message, ChatThread, User } from '../types';

interface ChatStore {
  currentUser: User | null;
  threads: Record<string, ChatThread>;
  activeThreadId: string | null;
  isComposing: boolean;
  typingUsers: Record<string, User[]>;
  preferences: {
    autoScroll: boolean;
    notificationSound: boolean;
    theme: 'light' | 'dark';
  };
  
  // Actions
  setCurrentUser: (user: User | null) => void;
  setActiveThread: (threadId: string | null) => void;
  addMessage: (threadId: string, message: Message) => void;
  updateMessageStatus: (threadId: string, messageId: string, status: Message['status']) => void;
  markThreadAsRead: (threadId: string) => void;
  setComposing: (isComposing: boolean) => void;
  addTypingUser: (threadId: string, user: User) => void;
  removeTypingUser: (threadId: string, userId: string) => void;
  updatePreferences: (preferences: Partial<ChatStore['preferences']>) => void;
}

const useChatStore = create<ChatStore>()(
  immer((set, get) => ({
    currentUser: null,
    threads: {},
    activeThreadId: null,
    isComposing: false,
    typingUsers: {},
    preferences: {
      autoScroll: true,
      notificationSound: true,
      theme: 'light',
    },
    
    setCurrentUser: (user) => set((state) => {
      state.currentUser = user;
    }),
    
    setActiveThread: (threadId) => set((state) => {
      state.activeThreadId = threadId;
      if (threadId && state.threads[threadId]) {
        // Mark as read when switching to thread
        state.threads[threadId].unreadCount = 0;
      }
    }),
    
    addMessage: (threadId, message) => set((state) => {
      const thread = state.threads[threadId];
      if (!thread) return;
      
      // Add to messages array
      thread.messages.push(message);
      
      // Update last message and unread count
      thread.lastMessage = message;
      if (threadId !== state.activeThreadId) {
        thread.unreadCount += 1;
      }
    }),
    
    updateMessageStatus: (threadId, messageId, status) => set((state) => {
      const thread = state.threads[threadId];
      if (!thread) return;
      
      const message = thread.messages.find(m => m.id === messageId);
      if (message) {
        message.status = status;
      }
    }),
    
    markThreadAsRead: (threadId) => set((state) => {
      const thread = state.threads[threadId];
      if (thread) {
        thread.unreadCount = 0;
      }
    }),
    
    setComposing: (isComposing) => set((state) => {
      state.isComposing = isComposing;
    }),
    
    addTypingUser: (threadId, user) => set((state) => {
      if (!state.typingUsers[threadId]) {
        state.typingUsers[threadId] = [];
      }
      
      const threadTypingUsers = state.typingUsers[threadId];
      const existingUserIndex = threadTypingUsers.findIndex(u => u.id === user.id);
      
      if (existingUserIndex === -1) {
        threadTypingUsers.push(user);
      }
    }),
    
    removeTypingUser: (threadId, userId) => set((state) => {
      if (!state.typingUsers[threadId]) return;
      
      const threadTypingUsers = state.typingUsers[threadId];
      const userIndex = threadTypingUsers.findIndex(u => u.id === userId);
      
      if (userIndex !== -1) {
        threadTypingUsers.splice(userIndex, 1);
      }
    }),
    
    updatePreferences: (preferences) => set((state) => {
      state.preferences = { ...state.preferences, ...preferences };
    }),
  }))
);

export default useChatStore;

// Custom hook for chat thread operations
export const useChatThread = (threadId: string) => {
  const { threads, activeThreadId } = useChatStore();
  const thread = threads[threadId];
  
  return {
    thread,
    isActive: activeThreadId === threadId,
    isUnread: thread?.unreadCount && thread.unreadCount > 0,
  };
};

// Custom hook for current user
export const useCurrentUser = () => {
  const { currentUser } = useChatStore();
  return currentUser;
};

The hook provides:

  1. Type-safe state access with proper TypeScript inference
  2. Optimistic UI updates for message sending and status changes
  3. Thread lifecycle management with automatic read marking
  4. Compositional state handling for typing indicators and preferences
  5. Clean API surface that's easy to test and maintain

Key state transitions:

  • Message sent → local optimistic update + server sync
  • Thread activated → unread count reset
  • User typing → real-time presence updates
  • Preferences changed → immediate UI refresh

This design allows for efficient state updates while maintaining a clean separation between server and client concerns, with proper error handling and optimistic update patterns.

4. Routing & Navigation

Route Structure and Hierarchy

graph TD
    A[Root] --> B[Public Routes]
    A --> C[Protected Routes]
    
    B --> D[/login]
    B --> E[/signup]
    B --> F[/forgot-password]
    
    C --> G[/chats]
    C --> H[/chats/:chatId]
    C --> I[/settings]

The chat application will have a protected routing structure with:

  • Public routes: /login, /signup, /forgot-password for authentication flows
  • Protected routes: /chats (main chat list), /chats/:chatId (individual chat view), /settings (user preferences)

Code Splitting Strategy

Route-based code splitting with React.lazy:

// routes.tsx
import { lazy } from 'react';

const Login = lazy(() => import('pages/Login'));
const Signup = lazy(() => import('pages/Signup'));
const ForgotPassword = lazy(() => import('pages/ForgotPassword'));
const ChatList = lazy(() => import('pages/ChatList'));
const ChatView = lazy(() => import('pages/ChatView'));
const Settings = lazy(() => import('pages/Settings'));

export const routes = [
  {
    path: '/login',
    element: <Login />,
    public: true,
  },
  {
    path: '/signup',
    element: <Signup />,
    public: true,
  },
  {
    path: '/forgot-password',
    element: <ForgotPassword />,
    public: true,
  },
  {
    path: '/chats',
    element: <ChatList />,
    protected: true,
  },
  {
    path: '/chats/:chatId',
    element: <ChatView />,
    protected: true,
  },
  {
    path: '/settings',
    element: <Settings />,
    protected: true,
  },
];

Justification: Route-based splitting provides optimal bundle loading for protected chat features, reducing initial load time while maintaining lazy loading benefits.

// AuthGuard.tsx
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from 'stores/auth';

export const AuthGuard = ({ children }: { children: React.ReactNode }) => {
  const navigate = useNavigate();
  const { isAuthenticated, isLoading } = useAuthStore();

  useEffect(() => {
    if (!isAuthenticated && !isLoading) {
      navigate('/login', { replace: true });
    }
  }, [isAuthenticated, isLoading, navigate]);

  if (isLoading) {
    return <div>Loading...</div>;
  }

  return children;
};

Deep Linking Support and URL Parameter Design

URL structure will follow RESTful conventions:

  • /chats - List of all chats
  • /chats/:chatId - Specific chat conversation
  • /chats/:chatId?messageId=123 - Deep link to specific message

URL parameters will be validated and sanitized:

// chatId validation
const validateChatId = (id: string): boolean => {
  return /^[a-zA-Z0-9_-]+$/.test(id);
};

Browser History Management and Scroll Restoration

// App.tsx
import { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';

const App = () => {
  const location = useLocation();
  const navigate = useNavigate();

  // Scroll to top on route change
  useEffect(() => {
    window.scrollTo(0, 0);
  }, [location.pathname]);

  // Handle back/forward navigation
  useEffect(() => {
    const handlePopState = () => {
      // Custom logic for chat state preservation
      if (location.pathname.startsWith('/chats/')) {
        // Restore chat view state from history
      }
    };

    window.addEventListener('popstate', handlePopState);
    return () => window.removeEventListener('popstate', handlePopState);
  }, [location]);

  return (
    <div>
      <Router>
        <ScrollRestoration />
        {routes.map((route) => (
          <Route
            key={route.path}
            path={route.path}
            element={
              route.protected ? (
                <AuthGuard>{route.element}</AuthGuard>
              ) : (
                route.element
              )
            }
          />
        ))}
      </Router>
    </div>
  );
};

The scroll restoration strategy ensures that when users navigate between chat conversations, the scroll position is preserved for better UX continuity. For deep links to specific messages, we'll implement a scroll-to-message feature that automatically scrolls to the target message when the chat view loads.

5. API Integration Layer

API Client Design Pattern

We'll implement a custom fetch wrapper with axios as the underlying HTTP client to provide consistent request handling and error management across all API calls. The client will be configured with base URL, default headers, and interceptors for authentication and error handling.

// api/client.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';

const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'https://api.example.com';

const createApiClient = (): AxiosInstance => {
  const client = axios.create({
    baseURL: API_BASE_URL,
    timeout: 10000,
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    },
  });

  // Request interceptor for adding auth token
  client.interceptors.request.use(
    (config) => {
      const token = localStorage.getItem('authToken');
      if (token) {
        config.headers.Authorization = `Bearer ${token}`;
      }
      return config;
    },
    (error) => Promise.reject(error)
  );

  // Response interceptor for handling errors
  client.interceptors.response.use(
    (response) => response,
    (error) => {
      if (error.response?.status === 401) {
        // Handle token expiration
        localStorage.removeItem('authToken');
        window.location.href = '/login';
      }
      return Promise.reject(error);
    }
  );

  return client;
};

export const apiClient = createApiClient();

Request/Response Interceptors

Authentication token injection, response normalization, and error transformation are handled through interceptors to ensure consistent behavior across all API calls.

Error Handling and Retry Strategy

  • Error Types: HTTP status codes (401, 403, 404, 429, 500+), network errors, and validation errors
  • Retry Strategy: Exponential backoff with max 3 retries for transient network errors (500, 502, 503, 429)
  • Error Boundaries: Global error handling with toast notifications for user feedback
// api/errorHandler.ts
import { AxiosError } from 'axios';

export class ChatApiError extends Error {
  constructor(
    public status: number,
    public message: string,
    public code?: string
  ) {
    super(message);
    this.name = 'ChatApiError';
  }
}

export const handleApiError = (error: AxiosError): ChatApiError => {
  if (error.response) {
    const { status, data } = error.response;
    return new ChatApiError(
      status,
      data?.message || `API Error: ${status}`,
      data?.code
    );
  }
  
  if (error.request) {
    return new ChatApiError(503, 'Network error - please check your connection');
  }
  
  return new ChatApiError(500, 'Unknown error occurred');
};

// Retry utility with exponential backoff
export const retryWithBackoff = async <T>(
  fn: () => Promise<T>,
  retries = 3,
  delay = 1000
): Promise<T> => {
  for (let i = 0; i <= retries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === retries) throw error;
      
      const shouldRetry = [500, 502, 503, 429].includes(
        (error as AxiosError).response?.status || 0
      );
      
      if (!shouldRetry) throw error;
      
      await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, i)));
    }
  }
  throw new Error('Retry limit exceeded');
};

Loading, Error, and Empty States Design

All chat operations will follow a consistent loading state pattern:

  • Loading: Spinner or skeleton UI during API fetches
  • Error: Toast notifications with retry options for recoverable errors
  • Empty: Placeholder UI when no messages or conversations exist

Data Fetching Strategy

For chat UI, we'll use Server-Side Rendering (SSR) for initial page load to improve perceived performance and SEO, with Client-Side Rendering (CSR) for real-time updates:

// pages/chat/[id].tsx - SSR with CSR fallback
export async function getServerSideProps(context: any) {
  const { id } = context.params;
  
  try {
    const [conversation, messages] = await Promise.all([
      apiClient.get(`/conversations/${id}`),
      apiClient.get(`/conversations/${id}/messages?limit=50`)
    ]);
    
    return {
      props: {
        initialConversation: conversation.data,
        initialMessages: messages.data,
      }
    };
  } catch (error) {
    return {
      notFound: true
    };
  }
}

API Response Transformation Layer

API responses are mapped to domain models for consistent frontend consumption:

// api/models.ts
export interface User {
  id: string;
  name: string;
  avatarUrl?: string;
}

export interface Message {
  id: string;
  content: string;
  senderId: string;
  timestamp: Date;
  status: 'sent' | 'delivered' | 'read';
}

export interface Conversation {
  id: string;
  title: string;
  participants: User[];
  lastMessage?: Message;
  unreadCount: number;
  updatedAt: Date;
}

// Response transformer
export const transformMessage = (raw: any): Message => ({
  id: raw.id,
  content: raw.content,
  senderId: raw.senderId,
  timestamp: new Date(raw.timestamp),
  status: raw.status || 'sent'
});

export const transformConversation = (raw: any): Conversation => ({
  id: raw.id,
  title: raw.title,
  participants: raw.participants.map((p: any) => ({
    id: p.id,
    name: p.name,
    avatarUrl: p.avatarUrl
  })),
  lastMessage: raw.lastMessage ? transformMessage(raw.lastMessage) : undefined,
  unreadCount: raw.unreadCount || 0,
  updatedAt: new Date(raw.updatedAt)
});

Sequence Diagram for Primary Data Flows

sequenceDiagram
    participant U as User
    participant C as Chat UI
    participant A as API Client
    participant S as Server

    U->>C: Load conversation
    C->>A: GET /conversations/:id
    A->>S: GET /conversations/:id
    S-->>A: Conversation data
    A-->>C: Response with conversation
    C->>A: GET /conversations/:id/messages?limit=50
    A->>S: GET /conversations/:id/messages?limit=50
    S-->>A: Messages data
    A-->>C: Response with messages

    U->>C: Send new message
    C->>A: POST /conversations/:id/messages
    A->>S: POST /conversations/:id/messages
    S-->>A: Message created
    A-->>C: Response with new message
    C->>C: Update local state with new message
    C->>A: GET /conversations/:id/messages?limit=50&after=:messageId
    A->>S: GET /conversations/:id/messages?limit=50&after=:messageId
    S-->>A: Updated messages list
    A-->>C: Response with updated messages

6. Performance Design

Core Web Vitals Targets

  • Largest Contentful Paint (LCP): <2.5s for chat messages and media
  • Interaction to Next Paint (INP): <200ms for user interactions (typing, sending messages)
  • Cumulative Layout Shift (CLS): <0.1 for chat interface elements
  • First Contentful Paint (FCP): <1.8s for initial chat load

Bundle Size Budget per Route

  • Chat Route: <250KB (including React, UI components, and chat-specific logic)
  • Message Composition: <50KB (for composer, emoji picker, file upload)
  • Media Preview: <100KB (image/video viewers and previews)
  • User Presence Indicators: <20KB (real-time status updates)

Critical Rendering Path Optimization

  • Critical CSS: Inline essential chat UI styles (message list, composer, header)
  • Async Loading: Non-critical components (emoji picker, file upload) loaded on demand
  • Preload Strategy: Preload chat font assets and key SVG icons (user avatars, status indicators)
  • Resource Hints: prefetch for next chat threads and preconnect to media CDN

Image Optimization Strategy

  • Formats: WebP for modern browsers, JPEG fallback for legacy support
  • Lazy Loading: loading="lazy" on all media elements with IntersectionObserver fallback
  • Responsive Images: srcset with multiple sizes for different device widths
  • Compression: Server-side optimization with progressive JPEGs and WebP conversion

Font Loading Strategy

  • Font Display: swap for chat UI fonts to ensure content renders quickly
  • Preload Critical Fonts: preload for primary font families (Roboto, Inter) in HTML head
  • Font Subsets: Load only Latin character subsets initially, load additional languages on demand
  • Fallback Fonts: Use system fonts as fallback to prevent FOIT (Flash of Invisible Text)

Memoization Strategy

  • React.memo: Wrap MessageItem and UserAvatar components with memoization
  • useCallback: Memoize event handlers for message sending, typing indicators, and media previews
  • useMemo: Cache computed values like formatted timestamps, user status, and message content truncation
  • Avoid Over-Memoization: Do not memoize simple props like messageId or userId

Virtualization for Large Lists

// ChatMessageList.tsx - Implementation using react-window
import { FixedSizeList as List } from 'react-window';

interface ChatMessage {
  id: string;
  content: string;
  senderId: string;
  timestamp: Date;
  type: 'text' | 'image' | 'file';
}

const ChatMessageList = ({ messages }: { messages: ChatMessage[] }) => {
  const itemSize = 80; // Approximate height of each message item
  
  return (
    <List
      height={600}
      itemCount={messages.length}
      itemSize={itemSize}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          <MessageItem message={messages[index]} />
        </div>
      )}
    </List>
  );
};

Prefetching and Preloading Strategy

  • Message History: Prefetch last 50 messages when chat thread loads
  • User Metadata: Preload user profiles for all participants in active chat threads
  • Media Assets: Preload media previews for the last 10 messages in each chat thread
  • Typing Indicators: Preload typing indicator animations and sounds (when enabled)
  • Offline Mode: Cache recent messages and media in IndexedDB for offline access

The strategy prioritizes loading critical chat content first, then progressively enhances the experience with additional features and media. Virtualization ensures smooth scrolling in large message lists, while preloading ensures that the most common interactions (typing, sending messages) are performant even with large datasets.

7. Error Handling & Resilience

Error Boundary Placement Strategy

Error boundaries should be strategically placed to prevent cascading failures while providing meaningful feedback to users. The placement strategy includes:

  1. ChatList Component: Error boundary around the entire chat list to handle loading failures or data corruption
  2. ChatWindow Component: Error boundary around the message display area to handle rendering issues
  3. MessageInput Component: Error boundary around input field to prevent crashes from malformed messages
  4. UserAvatar Component: Error boundary for individual user avatars to prevent one broken avatar from breaking the entire chat view
graph TD
    A[App] --> B[ChatList]
    A --> C[ChatWindow]
    A --> D[MessageInput]
    B --> E[ErrorBoundary]
    C --> F[ErrorBoundary]
    D --> G[ErrorBoundary]
    E --> H[ChatListErrorFallback]
    F --> I[ChatWindowErrorFallback]
    G --> J[MessageInputErrorFallback]

Global Error Handler Design

A global error handler will capture unhandled errors and provide appropriate user feedback:

// ErrorHandlerContext.tsx
import React, { createContext, useContext, useState, useEffect } from 'react';

interface ErrorContextType {
  error: Error | null;
  setError: (error: Error) => void;
  clearError: () => void;
}

const ErrorContext = createContext<ErrorContextType | undefined>(undefined);

export const useGlobalError = () => {
  const context = useContext(ErrorContext);
  if (!context) {
    throw new Error('useGlobalError must be used within ErrorProvider');
  }
  return context;
};

export const ErrorProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const handleError = (event: ErrorEvent) => {
      setError(event.error);
    };

    const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
      setError(event.reason);
    };

    window.addEventListener('error', handleError);
    window.addEventListener('unhandledrejection', handleUnhandledRejection);

    return () => {
      window.removeEventListener('error', handleError);
      window.removeEventListener('unhandledrejection', handleUnhandledRejection);
    };
  }, []);

  const clearError = () => {
    setError(null);
  };

  return (
    <ErrorContext.Provider value={{ error, setError, clearError }}>
      {children}
      {error && (
        <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
          <div className="bg-white p-6 rounded-lg shadow-xl max-w-md">
            <h2 className="text-xl font-bold text-red-600">Something went wrong</h2>
            <p className="mt-2 text-gray-700">{error.message}</p>
            <button 
              onClick={clearError}
              className="mt-4 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
            >
              Try Again
            </button>
          </div>
        </div>
      )}
    </ErrorContext.Provider>
  );
};

Offline / Network Failure Handling

The chat UI should gracefully handle network failures with:

  1. Offline Detection: Using navigator.onLine API to detect connection status
  2. Message Queue: Store outgoing messages in localStorage when offline
  3. Retry Logic: Implement exponential backoff for failed message sends
  4. Visual Indicators: Show offline status with clear messaging
// OfflineManager.ts
class OfflineManager {
  private queue: Message[] = [];
  private retryCount = 0;
  private maxRetries = 3;

  constructor() {
    window.addEventListener('online', this.handleOnline);
    window.addEventListener('offline', this.handleOffline);
  }

  handleOnline = () => {
    // Process queued messages
    this.processQueue();
  };

  handleOffline = () => {
    // Store current messages in queue
    this.queue = this.getPendingMessages();
  };

  processQueue = async () => {
    while (this.queue.length > 0 && this.retryCount < this.maxRetries) {
      const message = this.queue.shift();
      try {
        await this.send(message);
        this.retryCount = 0;
      } catch (error) {
        this.retryCount++;
        if (this.retryCount >= this.maxRetries) {
          // Move to failed queue or notify user
          this.handleFailedMessage(message);
        } else {
          // Retry with exponential backoff
          setTimeout(() => {
            this.queue.unshift(message);
          }, Math.pow(2, this.retryCount) * 1000);
        }
      }
    }
  };

  private getPendingMessages = (): Message[] => {
    // Implementation to retrieve pending messages from localStorage
    return [];
  };

  private send = async (message: Message): Promise<void> => {
    // Implementation to send message via API
  };

  private handleFailedMessage = (message: Message): void => {
    // Notify user about failed message
  };
}

Graceful Degradation Patterns

When features are unavailable, the chat should degrade gracefully:

  1. Read-only Mode: When real-time updates fail, show messages in read-only mode
  2. Local Cache Fallback: Use cached messages when network is unavailable
  3. Limited Features: Disable real-time typing indicators and read receipts when offline
  4. Alternative UI: Show simplified chat view with basic message display

User Feedback on Errors

Error feedback should be immediate and actionable:

  1. Toast Notifications: For network errors, show toast with retry option
  2. Inline Error Messages: Display error messages directly in chat input field
  3. Fallback UI: Show skeleton loaders or placeholder content while loading
  4. Retry Mechanisms: Provide clear retry actions when operations fail
// ToastManager.tsx
import React, { useState } from 'react';

interface Toast {
  id: string;
  message: string;
  type: 'error' | 'success' | 'warning';
  autoClose?: boolean;
}

const ToastManager: React.FC = () => {
  const [toasts, setToasts] = useState<Toast[]>([]);

  const addToast = (message: string, type: 'error' | 'success' | 'warning') => {
    const id = Date.now().toString();
    setToasts(prev => [...prev, { id, message, type }]);
    
    if (type === 'error') {
      setTimeout(() => {
        setToasts(prev => prev.filter(t => t.id !== id));
      }, 5000);
    }
  };

  const removeToast = (id: string) => {
    setToasts(prev => prev.filter(t => t.id !== id));
  };

  return (
    <div className="fixed top-4 right-4 space-y-2 z-50">
      {toasts.map(toast => (
        <div 
          key={toast.id}
          className={`p-4 rounded-lg shadow-lg max-w-xs ${
            toast.type === 'error' ? 'bg-red-100 text-red-800' :
            toast.type === 'success' ? 'bg-green-100 text-green-800' :
            'bg-yellow-100 text-yellow-800'
          }`}
        >
          <div className="flex justify-between items-center">
            <span>{toast.message}</span>
            <button 
              onClick={() => removeToast(toast.id)}
              className="ml-2 text-current"
            >
              ✕
            </button>
          </div>
        </div>
      ))}
    </div>
  );
};

8. Testing Strategy

Unit Testing Approach

Component Tests (70% coverage target):

  • Core chat components: ChatList, MessageItem, ChatInput
  • Utility functions: message formatting, user status handling
  • Custom hooks: useChatMessages, useUserStatus
  • Test coverage: 85% line coverage, 90% branch coverage
// Example: MessageItem component test
import { render, screen } from '@testing-library/react';
import { MessageItem } from './MessageItem';

describe('MessageItem', () => {
  const mockMessage = {
    id: '1',
    content: 'Hello world',
    senderId: 'user1',
    timestamp: new Date('2023-01-01T10:00:00Z'),
    status: 'sent'
  };

  it('renders message content correctly', () => {
    render(<MessageItem message={mockMessage} />);
    expect(screen.getByText('Hello world')).toBeInTheDocument();
  });

  it('applies correct styling for outgoing messages', () => {
    render(<MessageItem message={{...mockMessage, senderId: 'currentUser'}} />);
    const messageElement = screen.getByText('Hello world');
    expect(messageElement).toHaveClass('outgoing');
  });
});

Utility Function Tests:

// Message formatting utilities
describe('formatMessageTime', () => {
  it('formats time correctly for same day', () => {
    const time = new Date('2023-01-01T14:30:00Z');
    expect(formatMessageTime(time)).toBe('2:30 PM');
  });
});

Integration Testing Strategy

Component Integration Tests (50% coverage target):

  • ChatList with MessageItem rendering
  • ChatInput with message sending flow
  • User status updates in real-time context

API Integration Tests:

  • Message sending/receiving sequence
  • User status change propagation
  • Error handling for network failures

E2E Testing Scope

Tools: Playwright (parallel test execution, cross-browser support)

Critical User Journeys:

  1. User opens chat → sees conversation list
  2. User sends message → message appears in chat
  3. User receives new message → updates in real-time
  4. User navigates between chats → preserves state

Test Coverage:

  • 100% of core chat flows (message sending, receiving, typing indicators)
  • 80% of edge cases (offline scenarios, message retries)
  • 60% of accessibility flows (keyboard navigation, screen reader compatibility)

Visual Regression Testing

Tool: Chromatic (automated visual testing)

  • Test all chat UI states: empty chat, active chat, loading states
  • Validate RTL layout in Arabic/Farsi contexts
  • Monitor component rendering across browser versions

Accessibility Testing Automation

Tool: axe-core + testing-library

  • WCAG 2.1 AA compliance checks for all chat UI elements
  • Keyboard navigation flow validation
  • Screen reader announcement testing

Test Data Strategy

Test Fixtures:

// test/fixtures/chatData.ts
export const mockUsers = [
  { id: 'user1', name: 'Alice Johnson', status: 'online' },
  { id: 'user2', name: 'Bob Smith', status: 'offline' }
];

export const mockMessages = [
  {
    id: 'msg1',
    content: 'Hello there!',
    senderId: 'user1',
    timestamp: new Date('2023-01-01T10:00:00Z'),
    status: 'delivered'
  }
];

Data Generation Patterns:

  • Randomized message content for stress testing
  • Edge case scenarios (empty messages, long messages)
  • Multi-user conversation patterns

Test Environment Setup:

  • Isolated test databases per test suite
  • Mocked API responses with realistic delays
  • Simulated network conditions (slow 3G, offline)

9. Accessibility (A11y)

WCAG 2.1 AA compliance checklist for this feature

The chat UI must meet WCAG 2.1 AA standards with the following key requirements:

  • Text contrast: All text must have a 4.5:1 contrast ratio against background (minimum 3:1 for large text)
  • Keyboard navigation: Full functionality must be operable via keyboard alone
  • Focus management: Clear visual focus indicators for all interactive elements
  • Screen reader compatibility: Proper ARIA roles and semantic HTML structure
  • Alternative text: All non-text content must have accessible alternatives
  • Error identification: Clear error states with programmatically associated labels
  • Time limits: No time limits for user input unless essential to functionality

Keyboard navigation support

All chat UI interactions must be fully keyboard accessible:

  • Navigation: Tab/Shift+Tab to move between interactive elements (message input, send button, user list)
  • Message composition: Enter to send messages, Shift+Enter for new line
  • User list navigation: Arrow keys to navigate user list items, Enter to select
  • Message actions: Spacebar to toggle message reactions, keyboard shortcuts for common actions

Focus management strategy

Focus handling is critical in chat interfaces:

  • Auto-focus: Input field should receive focus when chat opens
  • Focus trapping: Within modals or dropdowns, focus stays within bounds
  • Focus restoration: When returning from nested views, previous focus context is restored
  • Visual indicators: Focus rings must be visible and consistent across all interactive elements

ARIA roles and attributes plan

// ChatMessage component ARIA implementation
interface ChatMessageProps {
  id: string;
  content: string;
  sender: string;
  timestamp: string;
  isOwnMessage?: boolean;
  status?: 'sent' | 'delivered' | 'read';
  reactions?: Reaction[];
}

// ARIA roles and attributes for chat messages
const ChatMessage = ({ 
  id, 
  content, 
  sender, 
  timestamp,
  isOwnMessage = false,
  status,
  reactions 
}: ChatMessageProps) => {
  return (
    <div 
      role="article"
      aria-labelledby={`message-${id}-header`}
      aria-describedby={`message-${id}-content`}
      tabIndex={0}
    >
      <div 
        id={`message-${id}-header`}
        role="heading"
        aria-level={3}
      >
        <span>{sender}</span>
        <span aria-label={`Sent at ${timestamp}`}>{timestamp}</span>
      </div>
      
      <div 
        id={`message-${id}-content`}
        role="region"
        aria-label={`${sender} said: ${content}`}
      >
        {content}
      </div>
      
      {status && (
        <span 
          role="status"
          aria-label={`Message status: ${status}`}
          className="message-status"
        >
          {status}
        </span>
      )}
      
      {reactions && reactions.length > 0 && (
        <div 
          role="group"
          aria-label={`Reactions to this message`}
          className="reactions-container"
        >
          {reactions.map((reaction) => (
            <span 
              key={reaction.emoji}
              role="button"
              aria-label={`Reaction: ${reaction.emoji} (${reaction.count})`}
              tabIndex={0}
              className="reaction-button"
            >
              {reaction.emoji} ({reaction.count})
            </span>
          ))}
        </div>
      )}
    </div>
  );
};

Screen reader compatibility

All interactive elements must provide appropriate screen reader context:

  • Live regions: Status updates (typing indicators, read receipts) use aria-live="polite"
  • Dynamic content: New messages are announced via aria-live="assertive"
  • Form controls: Proper labeling with aria-labelledby and aria-describedby
  • List navigation: User list items use role="listitem" with proper heading structure

Additional ARIA considerations

  • Chat container: role="main" or role="application" depending on context
  • Message list: role="list" with aria-live="polite"
  • Input field: Proper labeling with aria-label or aria-labelledby
  • Send button: aria-describedby to announce current message length limit
  • Status indicators: role="status" for real-time status updates

Focus management in multi-step flows

For complex interactions like message reactions or user selection:

// Example of focus management in reaction picker
const ReactionPicker = ({ 
  messageId, 
  onReactionSelect,
  onClose 
}: {
  messageId: string;
  onReactionSelect: (emoji: string) => void;
  onClose: () => void;
}) => {
  const reactionButtons = ['👍', '❤️', '😂', '😮', '😢', '😡'];
  const [focusedIndex, setFocusedIndex] = useState(-1);

  const handleKeyDown = (e: KeyboardEvent, index: number) => {
    if (e.key === 'Enter' || e.key === ' ') {
      onReactionSelect(reactionButtons[index]);
    } else if (e.key === 'Escape') {
      onClose();
    } else if (e.key === 'ArrowRight') {
      setFocusedIndex(prev => (prev + 1) % reactionButtons.length);
    } else if (e.key === 'ArrowLeft') {
      setFocusedIndex(prev => (prev - 1 + reactionButtons.length) % reactionButtons.length);
    }
  };

  return (
    <div 
      role="dialog"
      aria-labelledby="reaction-picker-title"
      aria-modal="true"
      className="reaction-picker"
    >
      <h3 id="reaction-picker-title">Add reaction</h3>
      <div className="reactions-grid">
        {reactionButtons.map((emoji, index) => (
          <button
            key={emoji}
            role="button"
            aria-label={`Add ${emoji} reaction`}
            tabIndex={focusedIndex === index ? 0 : -1}
            onKeyDown={(e) => handleKeyDown(e, index)}
            onClick={() => onReactionSelect(emoji)}
            className={focusedIndex === index ? 'focused' : ''}
          >
            {emoji}
          </button>
        ))}
      </div>
    </div>
  );
};

Keyboard accessibility patterns

  • Message input: Enter to send, Shift+Enter for new line
  • User list: Arrow keys navigate, Enter selects user
  • Reaction picker: Arrow keys navigate, Enter/Space to select
  • Scrollable areas: Keyboard navigation for long lists (PageUp/PageDown)
  • Modal dialogs: Escape key closes, Tab/Shift+Tab traps focus

Focus trapping implementation

// Focus trap utility for modals
const useFocusTrap = (modalRef: React.RefObject<HTMLElement>) => {
  useEffect(() => {
    const handleFocusTrap = (e: KeyboardEvent) => {
      if (!modalRef.current) return;
      
      const focusableElements = modalRef.current.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );
      
      if (focusableElements.length === 0) return;
      
      const firstElement = focusableElements[0] as HTMLElement;
      const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
      
      if (e.key === 'Tab') {
        if (document.activeElement === lastElement && !e.shiftKey) {
          e.preventDefault();
          firstElement.focus();
        } else if (document.activeElement === firstElement && e.shiftKey) {
          e.preventDefault();
          lastElement.focus();
        }
      }
    };

    document.addEventListener('keydown', handleFocusTrap);
    return () => document.removeEventListener('keydown', handleFocusTrap);
  }, [modalRef]);
};

ARIA live regions for real-time updates

// Status indicators with proper ARIA live regions
const ChatStatus = ({ 
  typingUsers, 
  readReceipts,
  connectionStatus 
}: {
  typingUsers: string[];
  readReceipts: {userId: string, timestamp: string}[];
  connectionStatus: 'online' | 'offline' | 'connecting';
}) => {
  return (
    <div className="chat-status">
      {typingUsers.length > 0 && (
        <span 
          role="status"
          aria-live="polite"
          className="typing-indicator"
        >
          {typingUsers.join(', ')} {typingUsers.length === 1 ? 'is' : 'are'} typing...
        </span>
      )}
      
      {connectionStatus === 'offline' && (
        <span 
          role="status"
          aria-live="assertive"
          className="connection-status offline"
        >
          Offline - reconnecting...
        </span>
      )}
      
      {readReceipts.length > 0 && (
        <div 
          role="status"
          aria-live="polite"
          className="read-receipts"
        >
          {readReceipts.map(({userId, timestamp}) => (
            <span key={userId}>
              {userId} read at {timestamp}
            </span>
          ))}
        </div>
      )}
    </div>
  );
};

Screen reader announcement patterns

  • New messages: aria-live="assertive" to interrupt current reading
  • Status changes: aria-live="polite" for non-critical updates
  • User actions: Announce when reactions are added or messages are sent
  • Error states: Clear announcements for input validation errors

Keyboard navigation flow

  1. Main chat view: Tab through header, message list, input field
  2. Message actions: Arrow keys to navigate reactions, Enter to select
  3. User list: Tab through users, Enter to open chat with user
  4. Modals: Escape to close, Tab to navigate within modal
  5. Long lists: PageUp/PageDown for quick navigation

ARIA attribute validation

All ARIA attributes must be:

  • Valid for the element they're applied to
  • Properly nested within semantic HTML structure
  • Not conflicting with native HTML semantics
  • Used only when they provide meaningful enhancement to accessibility

Testing approach

Accessibility testing will include:

  • Automated testing with axe-core and pa11y
  • Manual keyboard navigation testing across browsers
  • Screen reader testing (NVDA, JAWS, VoiceOver)
  • Focus order validation using keyboard navigation
  • Contrast ratio verification for all text elements

10. Security Considerations

XSS Prevention Strategy

The chat UI must implement robust XSS prevention to protect against malicious script injection in user messages:

// Secure message rendering component
const SafeMessageRenderer: React.FC<{ content: string }> = ({ content }) => {
  const sanitizeContent = (rawContent: string): string => {
    // Use DOMPurify or similar library for comprehensive sanitization
    const clean = DOMPurify.sanitize(rawContent, {
      ALLOWED_TAGS: ['b', 'i', 'u', 'em', 'strong'],
      ALLOWED_ATTR: [],
      KEEP_CONTENT: true,
    });
    
    // Additional validation to prevent script tags
    return clean.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
  };

  return (
    <div 
      dangerouslySetInnerHTML={{ __html: sanitizeContent(content) }} 
      className="chat-message-content"
    />
  );
};

CSRF Protection

All chat-related API requests must include CSRF tokens to prevent cross-site request forgery:

// Chat API client with CSRF protection
class ChatAPIClient {
  private csrfToken: string | null = null;

  async fetchWithCSRF<T>(url: string, options: RequestInit = {}): Promise<T> {
    const headers = new Headers(options.headers || {});
    
    if (!headers.has('X-CSRF-Token') && this.csrfToken) {
      headers.set('X-CSRF-Token', this.csrfToken);
    }
    
    const response = await fetch(url, {
      ...options,
      headers,
    });

    // Extract CSRF token from response headers if present
    const newCsrfToken = response.headers.get('X-CSRF-Token');
    if (newCsrfToken) {
      this.csrfToken = newCsrfToken;
    }

    return response.json();
  }
}

Content Security Policy (CSP) Headers

The chat feature requires strict CSP headers to prevent inline script execution:

Content-Security-Policy: 
  default-src 'self';
  script-src 'self' 'unsafe-inline' 'unsafe-eval';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' wss://chat.example.com;
  frame-ancestors 'none';

Sensitive Data Handling in Frontend State

User messages and metadata must be handled carefully to prevent accidental exposure:

// Secure chat state management with sensitive data handling
interface ChatState {
  messages: Message[];
  currentUser: User | null;
  typingUsers: string[]; // Only store user IDs, not names or PII
  unreadCount: number;
}

// Message type with sensitive data filtering
interface Message {
  id: string;
  content: string; // Sanitized content only
  senderId: string; // User ID, not name or email
  timestamp: Date;
  status: 'sent' | 'delivered' | 'read';
}

Input Sanitization Approach

All user inputs must be sanitized and validated before processing:

// Input validation and sanitization for chat messages
const validateAndSanitizeMessage = (message: string): string => {
  // Trim whitespace and enforce maximum length
  const trimmed = message.trim();
  
  if (trimmed.length === 0) {
    throw new Error('Message cannot be empty');
  }
  
  if (trimmed.length > 1000) {
    throw new Error('Message exceeds maximum length of 1000 characters');
  }
  
  // Remove potentially dangerous patterns
  const sanitized = trimmed.replace(/[<>'"&]/g, (char) => {
    switch (char) {
      case '<': return '&lt;';
      case '>': return '&gt;';
      case "'": return '&#x27;';
      case '"': return '&quot;';
      case '&': return '&amp;';
      default: return char;
    }
  });
  
  return sanitized;
};

Secure WebSocket Communication

For real-time chat features, ensure secure WebSocket connections:

// Secure WebSocket connection establishment
const establishSecureWebSocket = (userId: string, token: string) => {
  const wsUrl = new URL('/chat/ws', window.location.origin);
  wsUrl.protocol = wsUrl.protocol === 'https:' ? 'wss:' : 'ws:';
  
  // Add authentication token as query parameter (not in URL for security)
  const ws = new WebSocket(wsUrl.toString(), {
    headers: {
      'Authorization': `Bearer ${token}`,
      'X-User-ID': userId,
    }
  });

  ws.onopen = () => {
    console.log('Secure WebSocket connection established');
  };

  ws.onerror = (error) => {
    console.error('WebSocket error:', error);
    // Implement reconnection logic with exponential backoff
  };

  return ws;
};

Token Management and Session Handling

Chat authentication tokens must be handled securely:

// Secure token management with automatic refresh
class ChatTokenManager {
  private static readonly TOKEN_REFRESH_THRESHOLD = 5 * 60 * 1000; // 5 minutes
  
  private token: string | null = null;
  private tokenExpiry: Date | null = null;

  async refreshAuthToken(): Promise<string> {
    if (!this.token) {
      throw new Error('No authentication token available');
    }

    // Validate token before refresh
    if (this.tokenExpiry && Date.now() < this.tokenExpiry.getTime() - this.TOKEN_REFRESH_THRESHOLD) {
      return this.token;
    }

    // Refresh token via secure API call
    const response = await fetch('/api/auth/refresh', {
      method: 'POST',
      credentials: 'include', // Include cookies for session management
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': this.getCSRFToken(),
      },
    });

    if (!response.ok) {
      throw new Error('Failed to refresh authentication token');
    }

    const { token, expiresAt } = await response.json();
    this.token = token;
    this.tokenExpiry = new Date(expiresAt);
    
    return token;
  }

  private getCSRFToken(): string {
    // Extract CSRF token from meta tags or cookies
    const metaTag = document.querySelector('meta[name="csrf-token"]');
    return metaTag ? metaTag.getAttribute('content') || '' : '';
  }
}

Secure File Attachment Handling

If chat supports file attachments, ensure they're handled securely:

// Secure file attachment handling
const handleFileAttachment = async (file: File): Promise<{ url: string; size: number }> => {
  // Validate file type and size
  const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
  const maxSize = 10 * 1024 * 1024; // 10MB limit

  if (!allowedTypes.includes(file.type)) {
    throw new Error('File type not allowed');
  }

  if (file.size > maxSize) {
    throw new Error('File size exceeds maximum limit');
  }

  // Upload to secure, authenticated endpoint
  const formData = new FormData();
  formData.append('file', file);

  const response = await fetch('/api/chat/attachments', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${await getAuthToken()}`,
    },
    body: formData,
  });

  if (!response.ok) {
    throw new Error('Failed to upload attachment');
  }

  return response.json();
};

Secure Message Encryption (if applicable)

For end-to-end encrypted chat features:

// Basic encryption interface for secure messaging (conceptual)
interface SecureMessageHandler {
  encryptMessage(content: string, recipientKey: string): Promise<string>;
  decryptMessage(encryptedContent: string, senderKey: string): Promise<string>;
}

// Implementation would require:
// - Web Crypto API for encryption operations
// - Proper key exchange mechanisms (e.g., Diffie-Hellman)
// - Secure storage of encryption keys (e.g., in secure storage or memory only)

Security Headers Implementation

Ensure all chat-related pages include appropriate security headers:

// Server-side header configuration for chat pages
const chatSecurityHeaders = {
  'X-Content-Type-Options': 'nosniff',
  'X-Frame-Options': 'DENY',
  'X-XSS-Protection': '1; mode=block',
  'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
  'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' wss:;",
  'Referrer-Policy': 'strict-origin-when-cross-origin',
};

Authentication and Authorization

All chat operations must verify user identity and permissions:

// Chat authorization guard component
const ChatAuthorizationGuard: React.FC<{ 
  requiredPermissions: string[]; 
  children: React.ReactNode 
}> = ({ requiredPermissions, children }) => {
  const { user, isAuthenticated } = useAuth();
  
  // Check if user has required permissions
  const hasPermission = requiredPermissions.every(permission => 
    user?.permissions?.includes(permission)
  );

  if (!isAuthenticated || !hasPermission) {
    return <div>Access Denied</div>;
  }

  return <>{children}</>;
};

Secure Message Deletion

Implement secure message deletion with proper backend validation:

// Secure message deletion with confirmation
const deleteMessageSecurely = async (messageId: string, userId: string) => {
  // Client-side confirmation
  const confirmed = window.confirm('Are you sure you want to delete this message?');
  
  if (!confirmed) return;

  // Validate ownership before deletion request
  const message = getMessageById(messageId);
  if (message?.senderId !== userId) {
    throw new Error('You do not have permission to delete this message');
  }

  // Send deletion request to secure endpoint
  await fetch(`/api/chat/messages/${messageId}`, {
    method: 'DELETE',
    headers: {
      'Authorization': `Bearer ${await getAuthToken()}`,
      'X-User-ID': userId,
    },
  });
};

Rate Limiting and Abuse Prevention

Implement client-side rate limiting to prevent spam:

// Rate limiting for chat message sending
class ChatRateLimiter {
  private lastMessageTime: number = 0;
  private messageCount: number = 0;
  private readonly MAX_MESSAGES_PER_MINUTE = 20;

  canSendMessage(): boolean {
    const now = Date.now();
    
    // Reset counter if minute has passed
    if (now - this.lastMessageTime > 60000) {
      this.messageCount = 0;
      this.lastMessageTime = now;
    }

    return this.messageCount < this.MAX_MESSAGES_PER_MINUTE;
  }

  incrementMessageCount(): void {
    this.messageCount++;
  }
}

Secure User Identification

Ensure user identity is handled securely:

// Secure user identification in chat context
interface ChatUser {
  id: string; // UUID or secure identifier, never email or username
  displayName: string;
  avatarUrl?: string;
  status: 'online' | 'offline' | 'away';
}

// User ID should never be exposed in DOM or logs
const sanitizeUserId = (userId: string): string => {
  // Hash or obfuscate user IDs for logging purposes
  return btoa(userId).substring(0, 12); // Simple obfuscation for logs
};

11. Internationalization (i18n)

Translation Key Structure and Naming Convention

Following BEM-style naming with feature-scoped keys:

// Translation keys organized by feature scope
{
  "chat": {
    "header": {
      "title": "Chat",
      "searchPlaceholder": "Search conversations..."
    },
    "message": {
      "placeholder": "Type a message...",
      "sentBy": "{{user}} sent",
      "receivedBy": "{{user}} received"
    },
    "participant": {
      "online": "Online",
      "offline": "Offline"
    },
    "emptyState": {
      "noConversations": "No conversations yet",
      "startNew": "Start a new conversation"
    },
    "actions": {
      "send": "Send",
      "attach": "Attach",
      "emoji": "Emoji"
    }
  }
}

RTL Support Considerations

RTL layout handling through CSS classes and direction properties:

/* Apply RTL styles conditionally */
[dir="rtl"] .chat-container {
  flex-direction: row-reverse;
}

[dir="rtl"] .message-bubble {
  text-align: right;
}

/* Direction-aware flex properties */
.chat-header {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
}

Date, Number, Currency Formatting Strategy

Using Intl API for locale-aware formatting:

// Format timestamp with relative time
const formatRelativeTime = (date: Date, locale: string) => {
  return new Intl.RelativeTimeFormat(locale, { 
    style: 'short', 
    numeric: 'auto' 
  }).format(
    Math.floor((date.getTime() - Date.now()) / (1000 * 60)),
    'minute'
  );
};

// Format message timestamp
const formatTimestamp = (date: Date, locale: string) => {
  return new Intl.DateTimeFormat(locale, {
    hour: '2-digit',
    minute: '2-digit',
    hour12: true
  }).format(date);
};

Locale Detection and Switching Strategy

Automatic detection with manual override:

// Language detection logic
const detectUserLocale = (): string => {
  // Priority: browser lang → localStorage → default
  const savedLocale = localStorage.getItem('chat_locale');
  if (savedLocale) return savedLocale;
  
  const browserLang = navigator.language.split('-')[0];
  if (['en', 'es', 'fr', 'de', 'ja', 'ko'].includes(browserLang)) {
    return browserLang;
  }
  
  return 'en';
};

// Locale switcher component
const LocaleSwitcher = () => {
  const [currentLocale, setCurrentLocale] = useState(detectUserLocale());
  
  const handleLocaleChange = (newLocale: string) => {
    setCurrentLocale(newLocale);
    localStorage.setItem('chat_locale', newLocale);
    // Update i18n context and re-render
  };
  
  return (
    <select 
      value={currentLocale} 
      onChange={(e) => handleLocaleChange(e.target.value)}
    >
      <option value="en">English</option>
      <option value="es">Español</option>
      <option value="fr">Français</option>
      <option value="de">Deutsch</option>
      <option value="ja">日本語</option>
      <option value="ko">한국어</option>
    </select>
  );
};

Translation File Organization

Multiple translation files per language:

// en.json
{
  "chat": {
    "header": {
      "title": "Chat",
      "searchPlaceholder": "Search conversations..."
    }
  }
}

// es.json
{
  "chat": {
    "header": {
      "title": "Chat",
      "searchPlaceholder": "Buscar conversaciones..."
    }
  }
}

i18n Context Provider Implementation

// i18n context with fallback support
const I18nContext = createContext<{
  locale: string;
  translations: Record<string, any>;
  t: (key: string, params?: Record<string, any>) => string;
}>({
  locale: 'en',
  translations: {},
  t: (key) => key
});

const I18nProvider = ({ children }: { children: React.ReactNode }) => {
  const [locale, setLocale] = useState<string>('en');
  const [translations, setTranslations] = useState<Record<string, any>>({});
  
  useEffect(() => {
    const loadTranslations = async () => {
      try {
        const translationModule = await import(`./locales/${locale}.json`);
        setTranslations(translationModule.default);
      } catch (error) {
        console.error(`Failed to load translations for ${locale}`);
        setTranslations({});
      }
    };
    
    loadTranslations();
  }, [locale]);
  
  const t = useCallback((key: string, params?: Record<string, any>): string => {
    const keys = key.split('.');
    let value = translations;
    
    for (const k of keys) {
      value = value[k];
      if (!value) return key;
    }
    
    if (typeof value === 'string' && params) {
      return Object.entries(params).reduce((text, [key, val]) => 
        text.replace(`{{${key}}}`, String(val)), value);
    }
    
    return String(value);
  }, [translations]);
  
  return (
    <I18nContext.Provider value={{ locale, translations, t }}>
      {children}
    </I18nContext.Provider>
  );
};

12. Monitoring & Observability

Real User Monitoring (RUM) Metrics to Track

The chat UI should track key performance indicators to understand user experience and system health:

  • Chat Load Time: Time from component mount to first message display (target: <2s)
  • Message Send Latency: Time from user click to server acknowledgment (target: <500ms)
  • Typing Indicator Response: Time from user starts typing to indicator appears (target: <200ms)
  • Scroll Performance: Frame rate during message list scrolling (target: >50fps)
  • Connection Status Changes: Frequency and duration of reconnection events
  • Message Delivery Success Rate: Percentage of messages successfully delivered (target: >99%)

Error Tracking Integration

Integrate with Sentry or similar error tracking service to capture:

  • Uncaught Exceptions: Frontend errors that crash the chat experience
  • API Failures: Network errors during message fetch/send operations
  • Authentication Failures: Session expiration or token refresh issues
  • User Action Failures: Failed message sends, failed attachment uploads
  • Performance Degradation: Memory leaks, excessive re-renders

Custom Events and User Journey Tracking

Implement event tracking for key user flows:

// Example event tracking system
type ChatEvent = 
  | 'message_sent'
  | 'message_received'
  | 'typing_started'
  | 'typing_stopped'
  | 'attachment_uploaded'
  | 'chat_opened'
  | 'chat_closed'
  | 'user_joined'
  | 'user_left';

interface ChatEventPayload {
  messageId?: string;
  userId: string;
  timestamp: number;
  duration?: number; // For timing events
  metadata?: Record<string, any>;
}

const trackChatEvent = (event: ChatEvent, payload: ChatEventPayload) => {
  // Send to analytics service
  // Include session context, user ID, and chat room ID
};

Performance Monitoring and Regression Detection

Implement performance monitoring with:

  • Baseline Measurements: Establish performance baselines for chat interactions
  • Threshold Alerts: Configure alerts when performance degrades beyond 10% of baseline
  • A/B Testing Support: Track performance differences between chat UI versions
  • Memory Usage Monitoring: Track DOM node count and memory consumption for large message lists
  • Bundle Size Tracking: Monitor JavaScript bundle size changes that could impact chat load time
// Performance monitoring hook example
const useChatPerformance = () => {
  const [metrics, setMetrics] = useState<PerformanceMetrics>({
    loadTime: 0,
    messageSendLatency: 0,
    scrollPerformance: 0,
    memoryUsage: 0
  });

  const measureLoadTime = useCallback(() => {
    const start = performance.now();
    // Track chat component mount time
    return () => {
      const end = performance.now();
      setMetrics(prev => ({ ...prev, loadTime: end - start }));
    };
  }, []);

  return { metrics, measureLoadTime };
};

Key Metrics Dashboard

Create dashboard views for:

  • Real-time Chat Health: Connection status, message throughput
  • User Engagement Metrics: Active users, session duration, message volume
  • Error Rate Trends: API error rates, frontend crash rates over time
  • Performance Regression Alerts: Automated alerts when performance drops below thresholds

Integration with Existing Monitoring Infrastructure

  • Log Aggregation: Integrate with existing logging systems (e.g., ELK stack)
  • Infrastructure Monitoring: Connect to infrastructure monitoring (Prometheus, Grafana)
  • Business Intelligence: Send relevant chat metrics to BI tools for business insights
  • Alerting Rules: Configure alerting rules based on chat-specific thresholds

Data Privacy and Compliance Considerations

  • Data Minimization: Only collect necessary performance data
  • User Consent: Ensure performance tracking respects user privacy preferences
  • Anonymization: Remove or hash user identifiers in performance data
  • Compliance with GDPR/CCPA: Ensure monitoring practices comply with privacy regulations

Observability Tooling Stack

  • Frontend Monitoring: Sentry for error tracking, custom RUM for performance
  • Backend Integration: Connect to backend monitoring (APM tools like New Relic)
  • Real-time Dashboards: Grafana or custom dashboards for live chat metrics
  • Alerting System: Slack/Email alerts for critical performance degradation or error spikes

This observability strategy ensures that chat UI performance and reliability can be monitored in production, enabling quick detection and resolution of issues that impact user experience. The approach balances comprehensive monitoring with privacy considerations and performance overhead.

13. File & Folder Structure

Proposed Directory Structure

/src
├── components/
│   ├── chat/
│   │   ├── ChatContainer.tsx
│   │   ├── MessageList.tsx
│   │   ├── MessageInput.tsx
│   │   ├── ChatHeader.tsx
│   │   └── MessageItem.tsx
│   ├── ui/
│   │   ├── Avatar.tsx
│   │   ├── Button.tsx
│   │   └── Input.tsx
│   └── shared/
│       ├── ErrorBoundary.tsx
│       └── LoadingSpinner.tsx
├── features/
│   ├── chat/
│   │   ├── index.ts
│   │   ├── hooks/
│   │   │   └── useChat.ts
│   │   ├── store/
│   │   │   └── chatSlice.ts
│   │   └── types/
│   │       └── message.types.ts
├── hooks/
│   ├── useDebounce.ts
│   └── useScrollToBottom.ts
├── services/
│   ├── api/
│   │   └── chatApi.ts
│   └── transformers/
│       └── messageTransformer.ts
├── types/
│   ├── common.types.ts
│   └── chat.types.ts
├── utils/
│   ├── formatDate.ts
│   └── validateMessage.ts
└── styles/
    ├── globals.css
    └── components/
        └── chat.css

Module Boundaries and Import Rules

  • Feature isolation: Chat feature components live in /features/chat with their own hooks and store slices
  • Shared components: UI primitives like Avatar, Button live in /components/ui/
  • Cross-feature restrictions: No direct imports from other feature directories (e.g., no import { Button } from '../features/user')
  • Barrel exports: Feature directories export their public API through index.ts files for clean imports

Shared vs. Feature-Specific Code Separation

  • Shared: Common utilities, error boundaries, loading spinners, and base UI components that may be reused across features
  • Feature-specific: Chat-specific logic, types, and components that are only relevant to the chat functionality
  • Reusability: Components like Avatar and Button are shared across features, while MessageItem and ChatContainer are chat-specific

This structure follows a feature-based organization that scales well with growing component sets while maintaining clear module boundaries and reducing circular dependencies. The approach supports team ownership where different teams can own specific features without cross-contamination of code.

14. Dependencies & Third-Party Libraries

Key Libraries Selected with Justification

React (18.2+) - Core framework for building the UI. Provides the component model, hooks system, and concurrent rendering capabilities essential for smooth chat interactions.

TypeScript (5.0+) - Type safety ensures robustness in complex state management and component composition. Critical for maintaining code quality as chat features scale.

React Router DOM (6.15+) - For routing between chat rooms, user profiles, and settings. Supports nested routes and lazy loading for optimal bundle size.

Zustand (4.3+) - Light, fast state management for chat-specific global state (active room, user presence, unread counts). Avoids boilerplate of Redux while providing better performance than Context API for this use case.

React Query (5.0+) - For server state management and caching of chat messages, user data, and room metadata. Handles automatic refetching, background updates, and optimistic UI patterns.

Tailwind CSS (3.3+) - Utility-first CSS framework enabling rapid UI development with consistent design system integration and responsive behavior.

Framer Motion (10.12+) - For smooth animations in chat interactions (message entrance, typing indicators, scroll transitions) without bloating the bundle.

date-fns (2.30+) - Efficient date formatting and relative time calculations for message timestamps with minimal bundle size impact.

react-icons (4.10+) - Consistent iconography with tree-shaking support to reduce bundle size.

axios (1.5+) - HTTP client for API communication with interceptors for auth token handling and error normalization.

Alternatives Considered and Rejected

Redux Toolkit - While powerful, introduces unnecessary complexity for chat state management. Zustand provides similar functionality with less boilerplate and better performance.

Jotai - Overkill for this feature's state needs. Simpler solutions like Zustand or React Context are more appropriate.

Emotion - Tailwind CSS provides better developer experience and performance for chat UI components with consistent design system.

React Virtualized - Not needed for typical chat list sizes. react-window provides better performance characteristics for large lists.

Licensing and Bundle Size Impact Assessment

Core dependencies (React, TypeScript, Tailwind) - MIT licensed with minimal impact on bundle size when properly tree-shaken.

Zustand + React Query - MIT licensed, lightweight (~10KB total) with minimal impact on bundle size.

Framer Motion - MIT licensed, ~20KB bundle but provides significant UX value for chat animations.

date-fns - MIT licensed, ~2KB with modular imports reducing bundle size.

Bundle Size Budget Allocation

  • React + DOM: ~50KB
  • State Management (Zustand): ~5KB
  • UI Framework (Tailwind + React Icons): ~15KB
  • Data Fetching (React Query + Axios): ~20KB
  • Animations (Framer Motion): ~20KB
  • Date Utilities (date-fns): ~5KB
  • Total: ~115KB for core chat feature bundle

This allocation prioritizes performance and user experience while maintaining reasonable bundle sizes for fast initial load times.

15. Open Questions, Risks & Trade-offs

Decisions that need product/engineering alignment

  • Message persistence strategy: Should messages be stored locally for offline access, or rely entirely on server sync? This impacts both UX and storage architecture decisions.
  • Real-time vs. polling approach: How aggressively should we poll for new messages? What's the acceptable latency for message delivery?
  • Message retention policy: How long should messages be retained in UI state before being purged to prevent memory bloat?

Known trade-offs made in this design

  • Performance vs. feature richness: Prioritized rendering performance over complex message formatting (rich text, embedded media) to keep bundle size and render times manageable
  • State management complexity vs. scalability: Chose React Context + Zustand for simplicity over Redux Toolkit to reduce boilerplate, but acknowledged this may require refactoring as feature set grows
  • Client-side vs. server-side rendering: Opted for client-side rendering to enable real-time interactions, but this introduces hydration challenges and SEO considerations

Risks and mitigations

  • Memory leaks from message caching: Risk of unbounded growth in chat history state. Mitigation: Implement LRU cache with max message count and automatic cleanup
  • Network failure handling: Risk of messages being lost during network interruptions. Mitigation: Implement optimistic updates with local queue and retry mechanisms
  • Performance degradation in large conversations: Risk of UI lag when handling hundreds of messages. Mitigation: Implement virtualized lists and message pagination strategies

Technical debt acknowledged

  • Hardcoded UI state transitions: The current state machine approach uses hardcoded transitions that may become unwieldy as more complex chat flows are added
  • Limited message formatting support: Current design only handles basic text messages, requiring future refactoring to support rich media and complex message types
  • Monolithic chat component: The main Chat UI component is currently too large and may benefit from further decomposition into smaller, more focused components

Was this article helpful?

Topics in this article

More from Frontend LLD

  • Frontend LLD

    Comprehensive Video Player Implementation Guide

    Detailed low-level design for building a feature-rich video player with React, TypeScript, and modern web APIs

  • Frontend LLD

    Live Streaming Dashboard Design and Implementation

    Comprehensive low-level design for a real-time streaming dashboard with concurrent viewer tracking, live metrics, and scalable architecture.

  • Frontend HLD

    WhatsApp Web Frontend Architecture and Design Patterns

    Comprehensive high-level design document detailing WhatsApp Web's frontend architecture, component structure, and performance optimization strategies for enterprise-scale messaging applications.

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.