Overview
The Chat Applications examples demonstrate how to build messaging interfaces with Yoopta Editor. Two implementations are provided: Slack-style for team collaboration and WhatsApp-style for personal messaging.Slack Chat
Team messaging with channels and threads
Social Media Chat
WhatsApp/Instagram-style personal messaging
Slack-Style Chat
Features
Rich Text Composer
Rich Text Composer
- Text formatting (bold, italic, strikethrough)
- Code snippets with syntax highlighting
- @Mentions and emoji support
- Lists and quotes
Channel Interface
Channel Interface
- Channel list sidebar
- Message threads
- User presence indicators
- Timestamp formatting
Message Features
Message Features
- Reactions/emoji responses
- Message editing
- File attachments
- Read receipts
Implementation
Slack Chat Layout
import { useState } from 'react';
import { ScrollArea } from '@/components/ui/scroll-area';
function SlackChat() {
const [selectedChannel, setSelectedChannel] = useState('general');
const [messages, setMessages] = useState<Message[]>([]);
return (
<div className="flex h-screen">
{/* Channel Sidebar */}
<aside className="w-64 border-r bg-neutral-900 text-white">
<div className="p-4 font-semibold border-b border-neutral-800">
Workspace Name
</div>
<ChannelList
channels={channels}
selected={selectedChannel}
onSelect={setSelectedChannel}
/>
</aside>
{/* Main Chat Area */}
<main className="flex-1 flex flex-col">
{/* Channel Header */}
<div className="border-b p-4">
<h2 className="font-semibold"># {selectedChannel}</h2>
</div>
{/* Messages */}
<ScrollArea className="flex-1 p-4">
<MessageList messages={messages} />
</ScrollArea>
{/* Message Input */}
<div className="border-t p-4">
<MessageComposer onSend={handleSendMessage} />
</div>
</main>
</div>
);
}
Message Bubble Component
import { useMemo } from 'react';
import YooptaEditor, { createYooptaEditor } from '@yoopta/editor';
import { SLACK_PLUGINS, SLACK_MARKS } from './config';
function MessageBubble({ message }: { message: Message }) {
// Create read-only editor for each message
const previewEditor = useMemo(() => {
return createYooptaEditor({
plugins: SLACK_PLUGINS,
marks: SLACK_MARKS,
readOnly: true,
value: message.content,
});
}, [message.content]);
return (
<div className="flex gap-3 px-5 py-2.5 hover:bg-neutral-100">
{/* Avatar */}
<div className="w-9 h-9 rounded-lg bg-gradient-to-br from-green-500 to-emerald-600 flex items-center justify-center text-white font-semibold">
{message.author.avatar}
</div>
{/* Content */}
<div className="flex-1">
<div className="flex items-baseline gap-2">
<span className="font-semibold text-sm">
{message.author.name}
</span>
<span className="text-xs text-neutral-500">
{formatTime(message.timestamp)}
</span>
</div>
{/* Message rendered with Yoopta */}
<div className="text-sm">
<YooptaEditor editor={previewEditor} style={{ width: '100%' }} />
</div>
{/* Reactions */}
{message.reactions && (
<div className="flex gap-1 mt-1">
{message.reactions.map((reaction) => (
<button
key={reaction.emoji}
className="px-2 py-0.5 rounded border hover:bg-blue-50 text-xs"
>
{reaction.emoji} {reaction.count}
</button>
))}
</div>
)}
</div>
</div>
);
}
Message Composer
import { useMemo, useRef } from 'react';
import YooptaEditor, { createYooptaEditor } from '@yoopta/editor';
import { withMentions } from '@yoopta/mention';
import { withEmoji } from '@yoopta/emoji';
import { Button } from '@/components/ui/button';
import { Send, Bold, Italic, Code, AtSign, Smile } from 'lucide-react';
function MessageComposer({ onSend }: { onSend: (content: YooptaContentValue) => void }) {
const editor = useMemo(() => {
return withEmoji(
withMentions(
createYooptaEditor({
plugins: SLACK_PLUGINS,
marks: SLACK_MARKS,
})
)
);
}, []);
const handleSend = () => {
const content = editor.getEditorValue();
onSend(content);
// Clear editor
editor.setEditorValue({});
editor.focus();
};
return (
<div className="border rounded-lg">
{/* Formatting Toolbar */}
<div className="flex gap-1 p-2 border-b">
<Button
size="sm"
variant="ghost"
onClick={() => Marks.toggle(editor, { type: 'bold' })}
>
<Bold className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => Marks.toggle(editor, { type: 'italic' })}
>
<Italic className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => editor.toggleBlock('Code')}
>
<Code className="h-4 w-4" />
</Button>
</div>
{/* Editor */}
<div className="p-3">
<YooptaEditor
editor={editor}
placeholder="Message #general"
style={{ minHeight: 60 }}
/>
</div>
{/* Send Button */}
<div className="flex justify-end p-2 border-t">
<Button onClick={handleSend}>
<Send className="h-4 w-4 mr-2" />
Send
</Button>
</div>
</div>
);
}
Slack Plugin Configuration
import Paragraph from '@yoopta/paragraph';
import Lists from '@yoopta/lists';
import Code from '@yoopta/code';
import Blockquote from '@yoopta/blockquote';
import Link from '@yoopta/link';
import Mention from '@yoopta/mention';
import { Bold, Italic, Strike, CodeMark } from '@yoopta/marks';
export const SLACK_PLUGINS = [
Paragraph,
Lists.BulletedList,
Lists.NumberedList,
Code.extend({
options: {
languages: ['javascript', 'typescript', 'python', 'bash'],
},
}),
Blockquote,
Link,
Mention,
];
export const SLACK_MARKS = [Bold, Italic, Strike, CodeMark];
WhatsApp-Style Chat
Features
Message Bubbles
Message Bubbles
- Distinct styling for sent/received messages
- Bubble tails for conversation flow
- Message status indicators (sent, delivered, read)
- Timestamp display
Rich Media
Rich Media
- Image and video sharing
- File attachments
- Voice messages
- Link previews
Interactions
Interactions
- Emoji reactions
- Reply to messages
- Forward messages
- Delete for everyone
Implementation
Chat Interface
import { useState } from 'react';
import { Phone, Video, MoreVertical } from 'lucide-react';
function SocialMediaChat() {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [currentUser] = useState(CURRENT_USER);
return (
<div className="h-screen flex flex-col">
{/* Chat Header */}
<div className="border-b p-4 flex items-center justify-between bg-white">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center text-white font-semibold">
JD
</div>
<div>
<div className="font-semibold">John Doe</div>
<div className="text-xs text-neutral-500">Online</div>
</div>
</div>
<div className="flex gap-2">
<Button variant="ghost" size="sm">
<Phone className="h-5 w-5" />
</Button>
<Button variant="ghost" size="sm">
<Video className="h-5 w-5" />
</Button>
<Button variant="ghost" size="sm">
<MoreVertical className="h-5 w-5" />
</Button>
</div>
</div>
{/* Messages */}
<ScrollArea className="flex-1 p-4 bg-[#efeae2]">
<div className="space-y-2">
{messages.map((message, index) => {
const isOwn = message.senderId === currentUser.id;
const showAvatar = !isOwn && (
index === messages.length - 1 ||
messages[index + 1]?.senderId !== message.senderId
);
return (
<ChatBubble
key={message.id}
message={message}
isOwn={isOwn}
showAvatar={showAvatar}
/>
);
})}
</div>
</ScrollArea>
{/* Input */}
<div className="border-t p-4 bg-white">
<ChatInput onSend={handleSendMessage} />
</div>
</div>
);
}
Chat Bubble Component
function ChatBubble({
message,
isOwn,
showAvatar,
}: {
message: ChatMessage;
isOwn: boolean;
showAvatar: boolean;
}) {
const previewEditor = useMemo(() => {
return createYooptaEditor({
plugins: CHAT_PLUGINS,
marks: CHAT_MARKS,
readOnly: true,
value: message.content,
});
}, [message.content]);
return (
<div className={cn('flex gap-2 max-w-[80%]', isOwn ? 'ml-auto flex-row-reverse' : 'mr-auto')}>
{/* Avatar for received messages */}
{!isOwn && (
<div
className={cn(
'w-9 h-9 rounded-full flex items-center justify-center text-xs font-semibold text-white',
showAvatar ? 'visible' : 'invisible',
'bg-gradient-to-br from-violet-500 to-purple-600'
)}
>
{message.sender.avatar}
</div>
)}
{/* Message Bubble */}
<div className={cn('flex flex-col', isOwn ? 'items-end' : 'items-start')}>
<div
className={cn(
'relative px-4 py-2.5 rounded-2xl',
isOwn
? 'bg-blue-500 text-white rounded-br-md'
: 'bg-white rounded-bl-md'
)}
>
{/* Message Content */}
<div className="text-sm">
<YooptaEditor editor={previewEditor} style={{ width: '100%' }} />
</div>
{/* Reactions */}
{message.reactions && message.reactions.length > 0 && (
<div className={cn('absolute -bottom-3 flex gap-0.5', isOwn ? 'right-2' : 'left-2')}>
{message.reactions.map((reaction) => (
<span
key={reaction.emoji}
className="px-1.5 py-0.5 rounded-full bg-white border text-xs shadow-sm"
>
{reaction.emoji}
</span>
))}
</div>
)}
</div>
{/* Timestamp and Status */}
<div className={cn('flex items-center gap-1 mt-1 px-2', 'text-xs text-neutral-500')}>
<span>{formatTime(message.timestamp)}</span>
{isOwn && <MessageStatus status={message.status} />}
</div>
</div>
</div>
);
}
Chat Input Component
import { ImageCommands } from '@yoopta/image';
import { FileCommands } from '@yoopta/file';
import { Paperclip, Image, Send } from 'lucide-react';
function ChatInput({ onSend }: { onSend: (content: YooptaContentValue) => void }) {
const editor = useMemo(() => {
return withEmoji(
createYooptaEditor({
plugins: CHAT_INPUT_PLUGINS,
marks: CHAT_MARKS,
})
);
}, []);
const handleSend = () => {
const content = editor.getEditorValue();
if (Object.keys(content).length > 0) {
onSend(content);
editor.setEditorValue({});
}
};
const handleImageUpload = async (file: File) => {
const url = await uploadImage(file);
ImageCommands.insertImage(editor, { src: url });
};
return (
<div className="flex gap-2 items-end">
{/* Attachment buttons */}
<div className="flex gap-1">
<Button variant="ghost" size="sm">
<Paperclip className="h-5 w-5" />
</Button>
<label>
<input
type="file"
accept="image/*"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleImageUpload(file);
}}
/>
<Button variant="ghost" size="sm" asChild>
<span>
<Image className="h-5 w-5" />
</span>
</Button>
</label>
</div>
{/* Editor */}
<div className="flex-1 border rounded-3xl p-3 bg-white">
<YooptaEditor
editor={editor}
placeholder="Type a message..."
style={{ minHeight: 24 }}
/>
</div>
{/* Send button */}
<Button onClick={handleSend} className="rounded-full" size="icon">
<Send className="h-5 w-5" />
</Button>
</div>
);
}
Message Status Component
import { Check, CheckCheck } from 'lucide-react';
function MessageStatus({ status }: { status: 'sent' | 'delivered' | 'read' }) {
if (status === 'sent') {
return <Check className="w-3 h-3 text-neutral-400" />;
}
if (status === 'delivered') {
return <CheckCheck className="w-3 h-3 text-neutral-400" />;
}
return <CheckCheck className="w-3 h-3 text-blue-500" />;
}
Chat Plugin Configuration
import Paragraph from '@yoopta/paragraph';
import Image from '@yoopta/image';
import Video from '@yoopta/video';
import File from '@yoopta/file';
import Link from '@yoopta/link';
import { Bold, Italic, Strike } from '@yoopta/marks';
export const CHAT_PLUGINS = [
Paragraph,
Image,
Video,
File,
Link,
];
export const CHAT_INPUT_PLUGINS = [
Paragraph,
Image.extend({
options: {
async onUpload(file) {
const url = await uploadToCDN(file);
return { src: url };
},
},
}),
File,
];
export const CHAT_MARKS = [Bold, Italic, Strike];
Source Code
Slack Chat Source
View Slack-style implementation
Social Media Chat Source
View WhatsApp-style implementation
Use Cases
Team Collaboration
Build Slack-like team messaging platforms
Customer Support
Create live chat support systems
Social Messaging
Build WhatsApp-style personal messaging apps
Community Forums
Create discussion boards and forums
Next Steps
Collaboration
Add real-time collaboration with Yjs
Mentions
Learn about @mentions implementation