A complete real-time chat component with customizable UI elements and automatic message synchronization.
The Chat
component provides a fully functional real-time chat interface that synchronizes messages across all connected users. It includes a header, message list, input field, and can be minimized. All UI elements are customizable through the components
prop.
Perfect for: Team communication, customer support chat, collaborative comments, real-time discussions, and any application requiring instant messaging.
import { Chat } from 'react-together'
function MyApp() {
return (
<div className="app">
<main>
{/* Your app content */}
</main>
{/* Chat positioned in corner */}
<div className="fixed bottom-4 right-4 w-80 h-96">
<Chat rtKey="main-chat" chatName="Team Chat" />
</div>
</div>
)
}
Unique identifier for the chat session. All users with the same key will share the same chat
Display name shown in the chat header
Whether to hide the entire chat component when not connected to a session
Custom components to override default chat UI elements
The components
prop allows you to customize every part of the chat interface:
interface ChatComponents {
ChatMinimized?: React.ComponentType<ChatMinimizedProps>
ChatExpanded?: React.ComponentType<ChatExpandedProps>
ChatHeader?: React.ComponentType<ChatHeaderProps>
MessageList?: React.ComponentType<MessageListProps>
MessageRow?: React.ComponentType<MessageRowProps>
MessageAvatar?: React.ComponentType<MessageAvatarProps>
MessageBody?: React.ComponentType<MessageBodyProps>
ChatInput?: React.ComponentType<ChatInputProps>
ConnectPrompt?: React.ComponentType<unknown>
WelcomeMessage?: React.ComponentType<unknown>
}
Simple chat implementation for team communication:
import { Chat, useConnectedUsers } from 'react-together'
function TeamChat() {
const connectedUsers = useConnectedUsers()
return (
<div className="team-dashboard">
<div className="header">
<h1>Team Dashboard</h1>
<div className="user-count">
{connectedUsers.length} user(s) online
</div>
</div>
<div className="main-content">
{/* Your main app content */}
<div className="workspace">
<p>Main workspace content here...</p>
</div>
{/* Sidebar chat */}
<div className="chat-sidebar">
<Chat
rtKey="team-chat"
chatName="💬 Team Chat"
/>
</div>
</div>
</div>
)
}
Customize the chat header with additional information:
import { Chat, ChatHeaderProps, useConnectedUsers } from 'react-together'
function CustomChatHeader({ chatName, minimizeChat }: ChatHeaderProps) {
const connectedUsers = useConnectedUsers()
return (
<div className="custom-chat-header">
<div className="header-main">
<h3 className="chat-title">{chatName}</h3>
<button
onClick={minimizeChat}
className="minimize-btn"
title="Minimize chat"
>
−
</button>
</div>
<div className="header-info">
<span className="online-indicator">
🟢 {connectedUsers.length} online
</span>
<span className="chat-status">
Real-time chat active
</span>
</div>
</div>
)
}
function ChatWithCustomHeader() {
return (
<Chat
rtKey="team-discussion"
chatName="Team Discussion"
components={{
ChatHeader: CustomChatHeader
}}
/>
)
}
Create custom message bubbles with enhanced styling:
import { Chat, MessageRowProps, MessageBodyProps, useMyId } from 'react-together'
function CustomMessageBody({
senderId,
message,
sentAt,
isMe,
formatTime = (ts) => new Date(ts).toLocaleTimeString()
}: MessageBodyProps) {
return (
<div className={`message-bubble ${isMe ? 'my-message' : 'other-message'}`}>
<div className="message-content">
{message}
</div>
<div className="message-meta">
<span className="timestamp">
{formatTime(sentAt)}
</span>
{isMe && (
<span className="message-status">
✓ Sent
</span>
)}
</div>
</div>
)
}
function CustomMessageRow({
senderId,
message,
sentAt,
isMe,
MessageAvatar,
MessageBody
}: MessageRowProps) {
return (
<div className={`message-row ${isMe ? 'message-from-me' : 'message-from-other'}`}>
{!isMe && (
<div className="avatar-container">
<MessageAvatar senderId={senderId} isMe={isMe} />
</div>
)}
<div className="message-container">
<MessageBody
senderId={senderId}
message={message}
sentAt={sentAt}
isMe={isMe}
/>
</div>
{isMe && (
<div className="avatar-container">
<MessageAvatar senderId={senderId} isMe={isMe} />
</div>
)}
</div>
)
}
function StyledChat() {
return (
<Chat
rtKey="styled-chat"
chatName="🎨 Styled Chat"
components={{
MessageBody: CustomMessageBody,
MessageRow: CustomMessageRow
}}
/>
)
}
Enhanced chat input with additional features:
import { Chat, ChatInputProps } from 'react-together'
import { useState, useRef } from 'react'
function RichChatInput({ sendMessage }: ChatInputProps) {
const [input, setInput] = useState('')
const [isTyping, setIsTyping] = useState(false)
const inputRef = useRef<HTMLTextAreaElement>(null)
const handleSend = () => {
if (input.trim()) {
sendMessage(input.trim())
setInput('')
setIsTyping(false)
}
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSend()
}
}
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setInput(e.target.value)
setIsTyping(e.target.value.length > 0)
}
return (
<div className="rich-chat-input">
<div className="input-container">
<textarea
ref={inputRef}
value={input}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder="Type a message... (Enter to send, Shift+Enter for new line)"
className="message-textarea"
rows={1}
style={{
minHeight: '40px',
maxHeight: '120px',
resize: 'none'
}}
/>
<div className="input-actions">
<button
onClick={handleSend}
disabled={!input.trim()}
className={`send-button ${input.trim() ? 'active' : 'disabled'}`}
>
📤 Send
</button>
</div>
</div>
<div className="input-footer">
<div className="input-hints">
💡 Use Shift+Enter for line breaks
</div>
{isTyping && (
<div className="typing-indicator">
You are typing...
</div>
)}
</div>
</div>
)
}
function ChatWithRichInput() {
return (
<Chat
rtKey="rich-chat"
chatName="Rich Chat"
components={{
ChatInput: RichChatInput
}}
/>
)
}
Build a customer support chat with custom components:
import { Chat, useMyId, useConnectedUsers } from 'react-together'
function SupportChatHeader({ chatName, minimizeChat }: ChatHeaderProps) {
const connectedUsers = useConnectedUsers()
const supportAgents = connectedUsers.filter(user =>
user.nickname.includes('Agent') || user.nickname.includes('Support')
)
return (
<div className="support-chat-header">
<div className="header-content">
<div className="support-info">
<h3>🎧 Customer Support</h3>
{supportAgents.length > 0 ? (
<span className="agent-status">
✅ {supportAgents.length} agent(s) available
</span>
) : (
<span className="no-agents">
⏳ Waiting for agent...
</span>
)}
</div>
<button onClick={minimizeChat} className="minimize-btn">
−
</button>
</div>
</div>
)
}
function SupportMessageBody({
senderId,
message,
sentAt,
isMe
}: MessageBodyProps) {
const connectedUsers = useConnectedUsers()
const sender = connectedUsers.find(u => u.userId === senderId)
const isAgent = sender?.nickname.includes('Agent') || sender?.nickname.includes('Support')
return (
<div className={`support-message ${isMe ? 'customer-message' : 'agent-message'}`}>
{!isMe && isAgent && (
<div className="agent-badge">
🎧 Support Agent
</div>
)}
<div className="message-content">
{message}
</div>
<div className="message-time">
{new Date(sentAt).toLocaleTimeString()}
</div>
</div>
)
}
function SupportWelcomeMessage() {
return (
<div className="support-welcome">
<div className="welcome-icon">🎧</div>
<h3>Welcome to Support Chat</h3>
<p>A support agent will be with you shortly.</p>
<p>Please describe your issue in detail.</p>
</div>
)
}
function SupportChat() {
return (
<div className="support-widget">
<Chat
rtKey="support-chat"
chatName="Customer Support"
components={{
ChatHeader: SupportChatHeader,
MessageBody: SupportMessageBody,
WelcomeMessage: SupportWelcomeMessage
}}
/>
</div>
)
}
Create a chat system with multiple channels:
import { Chat } from 'react-together'
import { useState } from 'react'
interface ChatChannel {
id: string
name: string
description: string
emoji: string
}
function MultiChannelChat() {
const [activeChannel, setActiveChannel] = useState<ChatChannel>({
id: 'general',
name: 'General',
description: 'General discussion',
emoji: '💬'
})
const channels: ChatChannel[] = [
{ id: 'general', name: 'General', description: 'General discussion', emoji: '💬' },
{ id: 'dev-team', name: 'Development', description: 'Dev team chat', emoji: '⚡' },
{ id: 'design', name: 'Design', description: 'Design discussions', emoji: '🎨' },
{ id: 'random', name: 'Random', description: 'Off-topic chat', emoji: '🎲' }
]
return (
<div className="multi-channel-chat">
{/* Channel Selector */}
<div className="channel-selector">
<h3>Channels</h3>
<div className="channel-list">
{channels.map(channel => (
<button
key={channel.id}
onClick={() => setActiveChannel(channel)}
className={`channel-button ${
activeChannel.id === channel.id ? 'active' : ''
}`}
>
<span className="channel-emoji">{channel.emoji}</span>
<div className="channel-info">
<span className="channel-name">{channel.name}</span>
<span className="channel-description">{channel.description}</span>
</div>
</button>
))}
</div>
</div>
{/* Chat Area */}
<div className="chat-area">
<Chat
rtKey={`channel-${activeChannel.id}`}
chatName={
<div className="channel-header">
<span className="channel-emoji">{activeChannel.emoji}</span>
<span className="channel-name">#{activeChannel.name}</span>
</div>
}
/>
</div>
</div>
)
}
Enhanced chat with simulated file sharing:
import { Chat, ChatInputProps } from 'react-together'
import { useState } from 'react'
function FileShareChatInput({ sendMessage }: ChatInputProps) {
const [input, setInput] = useState('')
const [isUploading, setIsUploading] = useState(false)
const handleSend = () => {
if (input.trim()) {
sendMessage(input.trim())
setInput('')
}
}
const handleFileShare = async () => {
setIsUploading(true)
// Simulate file upload
setTimeout(() => {
sendMessage('📎 Shared a file: project-mockup.png')
setIsUploading(false)
}, 2000)
}
return (
<div className="file-share-input">
<div className="input-row">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleSend()
}
}}
className="message-input"
/>
<button
onClick={handleFileShare}
disabled={isUploading}
className="file-button"
title="Share file"
>
{isUploading ? '⏳' : '📎'}
</button>
<button
onClick={handleSend}
disabled={!input.trim()}
className="send-button"
>
Send
</button>
</div>
{isUploading && (
<div className="upload-progress">
📤 Uploading file...
</div>
)}
</div>
)
}
function FileShareChat() {
return (
<Chat
rtKey="file-share-chat"
chatName="📁 File Share Chat"
components={{
ChatInput: FileShareChatInput
}}
/>
)
}
Chat with message moderation features:
import { Chat, MessageBodyProps, useMyId } from 'react-together'
function ModeratedMessageBody({
senderId,
message,
sentAt,
isMe
}: MessageBodyProps) {
const myId = useMyId()
const isModerator = myId?.includes('moderator') || myId?.includes('admin')
const [isHidden, setIsHidden] = useState(false)
// Simple content filtering
const containsInappropriate = /\b(spam|inappropriate)\b/i.test(message)
return (
<div className={`moderated-message ${isMe ? 'my-message' : 'other-message'}`}>
{isHidden ? (
<div className="hidden-message">
<span>Message hidden by moderator</span>
{isModerator && (
<button onClick={() => setIsHidden(false)}>
Show
</button>
)}
</div>
) : (
<>
<div className={`message-content ${containsInappropriate ? 'flagged' : ''}`}>
{message}
{containsInappropriate && (
<span className="flag-indicator" title="Flagged content">
⚠️
</span>
)}
</div>
<div className="message-actions">
<span className="timestamp">
{new Date(sentAt).toLocaleTimeString()}
</span>
{isModerator && !isMe && (
<button
onClick={() => setIsHidden(true)}
className="moderate-button"
title="Hide message"
>
🚫
</button>
)}
</div>
</>
)}
</div>
)
}
function ModeratedChat() {
return (
<Chat
rtKey="moderated-chat"
chatName="🛡️ Moderated Chat"
components={{
MessageBody: ModeratedMessageBody
}}
/>
)
}
Each message contains:
interface ChatMessage {
id: number
senderId: string
message: string
sentAt: number
}
Messages automatically scroll to the bottom when new messages arrive.
The chat can be minimized to save screen space and expanded when needed.
The chat automatically handles disconnection states with appropriate UI feedback.
The Chat component includes default CSS classes that you can customize:
/* Main chat container */
.rt-chat { /* Chat wrapper */ }
/* Chat states */
.rt-chatContainer { /* Expanded chat */ }
/* Message area */
.rt-messageContainer { /* Message list container */ }
/* Input area */
.rt-input-container { /* Input wrapper */ }
.rt-input-text { /* Text input field */ }
.rt-input-button { /* Send button */ }
// ✅ Good - Use stable rtKey
<Chat rtKey="team-chat" />
// ❌ Bad - Dynamic rtKey causes reconnections
<Chat rtKey={`chat-${Math.random()}`} />
// ✅ Good - Descriptive chat name
<Chat rtKey="support" chatName="Customer Support Chat" />
// ✅ Good - Custom accessible components
const AccessibleChatInput = ({ sendMessage }: ChatInputProps) => (
<div className="chat-input">
<label htmlFor="message-input" className="sr-only">
Type your message
</label>
<input
id="message-input"
aria-label="Message input"
// ... other props
/>
<button aria-label="Send message">
Send
</button>
</div>
)
// ✅ Good - Hide when not needed
<Chat
rtKey="customer-support"
hideWhenDisconnected={true}
/>
// ✅ Good - Clear channel identification
<Chat
rtKey={`channel-${channelId}`}
chatName={`#${channelName}`}
/>
useChat
- The underlying hook powering the Chat componentuseConnectedUsers
- Get connected users for chat featuresuseMyId
- Identify current user in messagesuseNicknames
- User display namesThe Chat component is fully typed with comprehensive interfaces:
import { Chat, ChatProps, ChatComponents } from 'react-together'
const chatProps: ChatProps = {
rtKey: "typed-chat",
chatName: "Typed Chat",
hideWhenDisconnected: false,
components: {
// All component props are typed
}
}