A production-ready real-time chat component powered by Convex. Messages sync instantly across all connected clients with automatic scrolling and connection status indicators.
Installation
npx shadcn@latest add https://convex-ui.vercel.app/r/realtime-chat-react
This installs:
Chat UI components with message bubbles
Custom hooks for chat logic
Auto-scroll behavior
Complete Convex backend with message storage
Demo mode support (no auth required)
What’s Included
Real-time Sync Messages appear instantly across all connected clients
Auto-scroll Intelligent scrolling that preserves position when reading history
Connection Status Visual indicator showing connection state
Demo Mode Works without authentication for quick demos
Setup
Run the Convex development server:
Add your Convex URL to .env:
VITE_CONVEX_URL = https://your-deployment.convex.cloud
Ensure ConvexClientProvider wraps your app (automatically included):
import { ConvexClientProvider } from './lib/convex/provider'
ReactDOM . createRoot ( document . getElementById ( 'root' ) ! ). render (
< ConvexClientProvider >
< App />
</ ConvexClientProvider >
)
Component
RealtimeChat
The main chat component with message list and input.
import { RealtimeChat } from "@/components/realtime-chat" ;
function ChatRoom () {
return (
< div className = "h-screen" >
< RealtimeChat
roomName = "general"
username = "Alice"
/>
</ div >
);
}
Props
Unique identifier for the chat room. Users in the same room see the same messages.
Display name for the current user. Messages are labeled with this name.
Additional CSS classes for the chat container.
Usage Examples
Multiple Rooms
With Authentication
Custom Styling
import { RealtimeChat } from '@/components/realtime-chat' ;
import { useState } from 'react' ;
function ChatApp () {
const [ room , setRoom ] = useState ( 'general' );
return (
< div className = "h-screen flex flex-col" >
< div className = "flex gap-2 p-4 border-b" >
< button onClick = { () => setRoom ( 'general' ) } > General </ button >
< button onClick = { () => setRoom ( 'random' ) } > Random </ button >
< button onClick = { () => setRoom ( 'help' ) } > Help </ button >
</ div >
< RealtimeChat
roomName = { room }
username = "Alice"
className = "flex-1"
/>
</ div >
);
}
Custom Hooks
The component uses custom hooks that you can also use independently:
useRealtimeChat
Manages chat state and message sending.
import { useRealtimeChat } from '@/hooks/use-realtime-chat' ;
function CustomChat () {
const { messages , sendMessage , isConnected } = useRealtimeChat ({
roomName: 'my-room' ,
username: 'Alice' ,
});
return (
< div >
< div > Status: { isConnected ? 'Connected' : 'Connecting...' } </ div >
{ messages . map ( msg => (
< div key = { msg . id } >
< strong > { msg . user . name } : </ strong > { msg . content }
</ div >
)) }
< button onClick = { () => sendMessage ( 'Hello!' ) } >
Send
</ button >
</ div >
);
}
Arguments:
Current user’s display name
Returns:
{
messages : ChatMessage [],
sendMessage : ( content : string ) => Promise < void > ,
isConnected : boolean
}
Handles auto-scrolling behavior.
import { useChatScroll } from '@/hooks/use-chat-scroll' ;
import { useEffect } from 'react' ;
function CustomChatList ({ messages }) {
const { containerRef , scrollToBottom , shouldAutoScroll } = useChatScroll ();
useEffect (() => {
if ( shouldAutoScroll ()) {
scrollToBottom ();
}
}, [ messages ]);
return (
< div ref = { containerRef } className = "overflow-y-auto" >
{ messages . map ( msg => < div key = { msg . id } > { msg . content } </ div > ) }
</ div >
);
}
Returns:
{
containerRef : RefObject < HTMLDivElement > ,
scrollToBottom : () => void ,
handleScroll : () => void ,
shouldAutoScroll : () => boolean
}
Message Type
interface ChatMessage {
id : string ;
content : string ;
user : {
name : string ;
};
createdAt : string ; // ISO timestamp
}
Backend Functions
The chat component includes Convex backend functions:
list
Query all messages in a room (real-time).
api . messages . list ({ roomId: "general" })
Returns:
Array <{
_id : Id < "messages" >,
_creationTime : number ,
roomId : string ,
userId ?: Id < "users" >,
content : string ,
userName : string ,
sessionId ?: string
}>
send
Send a new message to a room.
api . messages . send ({
roomId: "general" ,
content: "Hello, world!" ,
userName: "Alice" ,
sessionId: "session-123" // Optional, for demo mode
})
Arguments:
Room identifier (max 100 chars)
Message text (max 2000 chars)
Session ID for demo mode (auto-generated)
Returns: Id<"messages"> - The created message ID
remove
Delete a message (only by owner).
api . messages . remove ({
messageId: "j97..." ,
sessionId: "session-123" // Required for demo mode
})
Features
Intelligent scrolling behavior:
Scrolls to bottom when sending a message
Scrolls to bottom when new messages arrive (if already at bottom)
Preserves scroll position when reading history
Connection Status
Visual indicator in the header:
Green badge: “Connected”
Red badge: “Disconnected”
Demo Mode
Works without authentication:
Each browser tab gets a unique session ID
Session ID stored in localStorage
Messages associated with session, not user
Message Validation
Backend validates all messages:
Content trimmed and limited to 2000 chars
Room ID limited to 100 chars
Username sanitized (trimmed, max 50 chars)
Empty messages rejected
Customization
Custom Message Bubbles
Modify components/chat-message.tsx:
export function ChatMessage ({ message , isOwnMessage }) {
return (
< div className = { cn (
"flex" ,
isOwnMessage ? "justify-end" : "justify-start"
) } >
< div className = { cn (
"rounded-lg px-4 py-2 max-w-[70%]" ,
isOwnMessage
? "bg-primary text-primary-foreground"
: "bg-muted"
) } >
{ ! isOwnMessage && (
< div className = "text-xs font-semibold mb-1" >
{ message . user . name }
</ div >
) }
< p className = "text-sm" > { message . content } </ p >
</ div >
</ div >
);
}
The input is part of the RealtimeChat component. Extract it to customize:
< form onSubmit = { handleSubmit } >
< div className = "flex gap-2" >
< Input
value = { inputValue }
onChange = { ( e ) => setInputValue ( e . target . value ) }
placeholder = "Type a message..."
/>
< Button type = "submit" >
< Send className = "w-4 h-4" />
</ Button >
</ div >
</ form >
Schema
The messages table schema:
import { defineSchema , defineTable } from "convex/server" ;
import { v } from "convex/values" ;
export default defineSchema ({
messages: defineTable ({
roomId: v . string (),
userId: v . optional ( v . id ( "users" )),
content: v . string (),
userName: v . string (),
sessionId: v . optional ( v . string ()),
}). index ( "by_room" , [ "roomId" ]) ,
}) ;
Security
The demo mode allows anonymous messaging. For production apps with authentication, modify the backend functions to require getAuthUserId() and validate user permissions.
Security features:
Content length limits prevent abuse
Input sanitization (trimming, length caps)
Room ID validation
Message ownership tracking (userId or sessionId)
Optimized for scale:
Uses Convex indexes for fast room queries
Real-time subscriptions are efficient (only sends changes)
Auto-scroll logic minimizes unnecessary re-renders
Messages loaded on-demand per room
Troubleshooting
Messages not appearing in real-time
Verify npx convex dev is running and VITE_CONVEX_URL is set correctly. Check browser console for connection errors.
Ensure the chat container has a fixed height. The scroll behavior requires a scrollable container with overflow-y-auto.
Connection shows as disconnected
Check that the Convex client is properly initialized and the provider wraps your app. The isConnected state reflects whether the query has loaded.
Next Steps
Realtime Cursors Add collaborative cursor tracking
Avatar Stack Show who’s in the room
Password Auth Authenticate users before chatting