Chat Conversation Block
ChatConversation is a responsive, highly customizable messaging interface built on MUI Stack and TextField inputs, supporting day-grouped message streams, mobile-optimized overflow menus, infinite-scroll pagination hooks, and layout composition.
Installation
- npm
- yarn
- pnpm
- bun
npm install @nodeblocks/frontend-chat-conversation-block
yarn add @nodeblocks/frontend-chat-conversation-block
pnpm add @nodeblocks/frontend-chat-conversation-block
bun add @nodeblocks/frontend-chat-conversation-block
What You Need
| Item | Why it matters |
|---|---|
messages | Array of message bubbles to display; the block groups and sorts them by createdAt |
chatView | Chat panel configurations controlling headings, avatars, mobile overflow menus, and loader indicators |
labels | Text labels for the heading title, optional subheading, and desktop action button |
placeholders | Input placeholders (such as message comment input text) |
onNavigate | Callback hook triggered when clicking navigation headers or linking avatars |
ChatConversation is presentational and does not manage active state lists. Maintain local messages state in your application, update the array inside onMessageSubmit, and toggle page load spinners with chatView.isLoading.
Code Examples
- Quick Start
- Labels and URLs
- Compound Components
- Block Override
function Example() { const [messages, setMessages] = React.useState([ { id: 'msg-1', title: 'John Doe', content: 'Hey! Are we still on for the project review today?', createdAt: new Date(Date.now() - 3600000).toISOString(), isOwnMessage: false, }, { id: 'msg-2', title: 'You', content: 'Yes! I have the slides ready.', createdAt: new Date(Date.now() - 1800000).toISOString(), isOwnMessage: true, }, ]); const [isLoading, setIsLoading] = React.useState(false); const [lastEvent, setLastEvent] = React.useState('Ready'); const handleMessageSubmit = text => { setLastEvent(`Sent: ${text}`); // Add user message const userMsg = { id: `msg-${Date.now()}`, title: 'You', content: text, createdAt: new Date().toISOString(), isOwnMessage: true, }; setMessages(prev => [...prev, userMsg]); // Simulate reply setIsLoading(true); setTimeout(() => { setMessages(prev => [ ...prev, { id: `reply-${Date.now()}`, title: 'John Doe', content: 'Awesome, see you in the meeting room!', createdAt: new Date().toISOString(), isOwnMessage: false, avatar: {children: 'JD'}, }, ]); setIsLoading(false); }, 1500); }; return ( <div style={{height: 500, border: '1px solid #e0e0e0', borderRadius: 8, overflow: 'hidden'}}> <ChatConversation chatView={{ heading: { avatar: {children: 'JD'}, buttonHref: '#home', }, isLoading: isLoading, }} labels={{ chatViewHeadingButtonText: 'Home', chatViewHeadingText: 'Project Chat', chatViewHeadingSubText: 'Review team', }} placeholders={{ commentInput: 'Type your message...', }} messages={messages} onMessageSubmit={handleMessageSubmit} onNavigate={url => setLastEvent(`Navigating to: ${url}`)} /> <div style={{marginTop: 8, fontSize: 12, color: '#666'}}>{lastEvent}</div> </div> ); }
Configure active loading indicators, mobile overflow menus with custom click actions, back buttons, and input placeholders.
function Example() { const [messages, setMessages] = React.useState([ { id: 'msg-1', title: 'Support Agent', content: 'Hello! How can I help you today?', createdAt: new Date(Date.now() - 3600000).toISOString(), isOwnMessage: false, avatar: {children: 'SA', sx: {bgcolor: 'secondary.main'}}, }, ]); const [lastEvent, setLastEvent] = React.useState('Ready'); const handleMessageSubmit = text => { setLastEvent(`Sent: ${text}`); setMessages(prev => [ ...prev, { id: `msg-${Date.now()}`, title: 'You', content: text, createdAt: new Date().toISOString(), isOwnMessage: true, }, ]); }; const chatViewConfig = { heading: { avatar: {children: 'SA', sx: {bgcolor: 'secondary.main'}}, buttonHref: '/support/dashboard', onBack: () => setLastEvent('Back clicked on mobile'), menuItems: [ { label: 'View Profile', icon: <PersonOutlined fontSize="small" />, onClick: () => setLastEvent('Viewing agent profile'), }, { label: 'Close Ticket', icon: <NotificationsNone fontSize="small" />, onClick: () => setLastEvent('Ticket closed'), }, ], }, isLoading: false, }; return ( <div style={{height: 500, border: '1px solid #e0e0e0', borderRadius: 8, overflow: 'hidden'}}> <ChatConversation chatView={chatViewConfig} labels={{ chatViewHeadingButtonText: 'Support', chatViewHeadingText: 'Customer Support', chatViewHeadingSubText: 'Ticket #14052', }} placeholders={{ commentInput: 'Describe your issue...', }} messages={messages} onMessageSubmit={handleMessageSubmit} onNavigate={url => setLastEvent(`Redirecting to: ${url}`)} /> <div style={{marginTop: 8, fontSize: 12, color: '#666'}}>{lastEvent}</div> </div> ); }
Use chatView.heading.menuItems to supply custom mobile actions (such as closing, sharing, or flagging a thread). Subheading strings displayed under labels.chatViewHeadingSubText wrap gracefully to a secondary mobile lines and combine into standard title tracks on desktop layouts.
Compose the layout using fine-grained compound child blocks. The root still receives the required context props, and child props can override specific sections.
function Example() { const [messages, setMessages] = React.useState([ { id: 'msg-1', title: 'John Doe', content: 'Hey! Here is the link to the design: Figma.com/file/123', createdAt: new Date().toISOString(), isOwnMessage: false, }, ]); const [lastEvent, setLastEvent] = React.useState('Ready'); return ( <div style={{height: 500, border: '1px solid #e0e0e0', borderRadius: 8, overflow: 'hidden'}}> <ChatConversation chatView={{ heading: { avatar: {children: 'JD'}, buttonHref: '#back', }, }} labels={{ chatViewHeadingButtonText: 'Back', chatViewHeadingText: 'Design Thread', }} placeholders={{ commentInput: 'Reply to design thread...', }} messages={messages} onNavigate={url => setLastEvent(`Navigating: ${url}`)} > <ChatConversation.Heading chatView={{ heading: { avatar: {children: 'JD'}, buttonHref: '#back', }, }} labels={{ chatViewHeadingButtonText: 'Back', chatViewHeadingText: 'Design Thread', }} /> <ChatConversation.Body messages={messages} placeholders={{ commentInput: 'Reply to design thread...', }} onMessageSubmit={text => { setLastEvent(`Sent: ${text}`); setMessages(prev => [ ...prev, { id: String(Date.now()), title: 'You', content: text, createdAt: new Date().toISOString(), isOwnMessage: true, }, ]); }} /> </ChatConversation> <div style={{marginTop: 8, fontSize: 12, color: '#666'}}>{lastEvent}</div> </div> ); }
Reorder blocks or inject system notice alerts between heading and conversation sections using the standard function children override block mapping.
function Example() { const [messages, setMessages] = React.useState([ { id: 'msg-1', title: 'Bot', content: 'Welcome! How can I assist you today?', createdAt: new Date().toISOString(), isOwnMessage: false, }, ]); return ( <div style={{height: 500, border: '1px solid #e0e0e0', borderRadius: 8, overflow: 'hidden'}}> <ChatConversation chatView={{ heading: { buttonHref: '#home', }, }} labels={{ chatViewHeadingButtonText: 'Exit', chatViewHeadingText: 'Assistant Bot', }} placeholders={{ commentInput: 'Type a query...', }} messages={messages} onMessageSubmit={text => { setMessages(prev => [ ...prev, { id: String(Date.now()), title: 'You', content: text, createdAt: new Date().toISOString(), isOwnMessage: true, }, ]); }} onNavigate={url => void url} > {({defaultBlocks, defaultBlockOrder}) => ({ blocks: { ...defaultBlocks, systemAlert: ( <div style={{ background: '#fffbe6', borderBottom: '1px solid #ffe58f', padding: '8px 16px', fontSize: '12px', color: '#d46b08', textAlign: 'center', }} > This is a secure automated workspace assistant session. </div> ), }, blockOrder: ['heading', 'systemAlert', 'body'], })} </ChatConversation> <div style={{marginTop: 8, fontSize: 12, color: '#666'}}>System notice inserted between blocks.</div> </div> ); }
When to use block overrides
Use overrides to prepend banners, system statuses, security disclosures, or attachment action trays inside the conversation viewport. The default block order is ['heading', 'body'] (defaultBlockOrder).
Important Props
Core Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
messages | Array<{ id: string; content: string; createdAt: string; title?: string; isOwnMessage?: boolean; avatar?: object }> | Yes | - | Array of chat message records grouped and sorted by createdAt |
onMessageSubmit | (text: string) => void | No | undefined | Callback triggered when submitting input via the text field |
onInputChange | (text: string) => void | No | undefined | Callback triggered when comment input text is edited |
Message Object
Configure properties inside the messages list array:
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
id | string | Yes | - | Unique message record identifier |
content | string | Yes | - | Core content text displayed inside the chat bubble |
createdAt | string | Yes | - | Date-time timestamp (ISO string format) used for day separator groupings |
isOwnMessage | boolean | No | false | Highlight own comments aligning bubbles to the right viewport edge when true |
title | string | No | undefined | Display author header title above the chat bubble |
avatar | object | No | undefined | Individual comment Avatar config: MUI Avatar props plus optional href, isLoading, and link/click behavior (component="a" is supported) |
Content Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
chatView | { heading?: { avatar?: object; buttonHref: string; onBack?: () => void; menuItems?: Array<{ label: string; href?: string; icon?: ReactNode; onClick?: (e: MouseEvent<HTMLElement>) => void }> }; isLoading?: boolean; onScrollTop?: (earliestId: string) => void } | Yes | - | Panel layouts configuring headings, mobile menus, back clicks, and infinite scrolling; heading avatars accept standard MUI Avatar props, including linked avatars via component="a" + href |
labels | { chatViewHeadingButtonText?: string; chatViewHeadingText?: string; chatViewHeadingSubText?: string } | Yes | - | Main heading labels and exit button text |
placeholders | { commentInput: string } | Yes | - | Wording configurations for inputs |
onNavigate | (url: string) => void | Yes | - | Standard redirect hook mapping clicked elements and settings |
commentInput | { isDisabled?: boolean; errorText?: string } | No | undefined | Configuration for bottom comment inputs |
commentSubmitButton | { isDisabled?: boolean } | No | undefined | Active states for the send button |
chatView Structure
Configure layout heading indicators inside chatView:
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
heading | { avatar?: object; buttonHref: string; onBack?: () => void; menuItems?: ChatConversationHeadingMenuItem[] } | No | undefined | Configuration for heading actions. buttonHref is required when heading is provided. |
isLoading | boolean | No | false | Displays top pagination loaders or full viewport spinners during fetches |
onScrollTop | (earliestId: string) => void | No | undefined | Callback triggered when scrolling to top (used for loading previous history) |
Menu Item Object
Option configurations for heading overflow dropdown menu lists (menuItems):
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
label | string | Yes | - | Label text displayed on the menu option |
href | string | No | undefined | Redirect target mapped via onNavigate |
icon | ReactNode | No | undefined | Icon element prepended to menu option labels |
onClick | (e: MouseEvent<HTMLElement>) => void | No | undefined | Handler triggered when clicking the option |
Layout and Composition Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
children | BlocksOverride | ReactNode | No | undefined | Compound child elements or override function child |
className | string | No | undefined | Styling class applied on the outer container |
sx | SxProps | No | undefined | MUI SX styling overrides passed to the outer Stack |
ChatConversation inherits all StackProps (except children). Default block order is ['heading', 'body'] (defaultBlockOrder).
Default UI Blocks
| Block | Built on | Notes |
|---|---|---|
ChatConversation (root) | Stack | Stacks heading and body panels vertically |
ChatConversation.Heading | Stack | Displays back button, avatar details, combined titles, and exit buttons |
ChatConversation.Body | Stack | Groups messages by day, sorts by createdAt, triggers onScrollTop boundaries, and mounts the input |
TypeScript
import {ChatConversation} from '@nodeblocks/frontend-chat-conversation-block';
import type {MouseEvent, ReactNode} from 'react';
type ChatMessage = {
id: string;
content: string;
createdAt: string;
title?: string;
isOwnMessage?: boolean;
avatar?: {
component?: 'a';
onClick?: (event: MouseEvent<HTMLElement>) => void;
children?: ReactNode;
src?: string;
srcSet?: string;
href?: string;
isLoading?: boolean;
sx?: object;
};
};
type MenuItem = {
label: string;
href?: string;
icon?: ReactNode;
onClick?: (event: MouseEvent<HTMLElement>) => void;
};
const messages: ChatMessage[] = [
{
id: 'msg_001',
title: 'Customer support',
content: 'We are processing your request. Please hold on.',
createdAt: '2026-05-27T02:00:00Z',
isOwnMessage: false,
avatar: {children: 'CS', sx: {bgcolor: 'primary.main'}},
},
];
const menuItems: MenuItem[] = [{label: 'View Profile', onClick: () => void 0}];
<ChatConversation
chatView={{
heading: {
buttonHref: '/support',
menuItems,
},
isLoading: false,
}}
labels={{
chatViewHeadingButtonText: 'Exit',
chatViewHeadingText: 'Customer Care',
}}
placeholders={{
commentInput: 'Describe your issue...',
}}
messages={messages}
onMessageSubmit={text => void text}
onNavigate={url => void url}
/>;