Components
Every slot you can replace, how defaults fill in, and every classNames key.
Default component fallback
Customization is opt-in per slot. You never register every component - pass only the ones you want to replace through components on ChatMain (or uiConfig.components on Chat).
ChatMain components={{ MembersDrawer: MyDrawer }} ↓ChatCustomizationProvider merges your config ↓At render: resolveComponent("MembersDrawer", PackageMembersDrawer) ↓Custom passed? → use MyDrawerNot passed? → use PackageMembersDrawer (built-in default)Your component receives the same props as the package default (MembersDrawerSlotProps, etc.) - import the prop type from the package for TypeScript.
Which slots are wired
These slots are honored at runtime when you pass them:
Advanced slots
Typed and exported, but used inside inner components - customize them by replacing the parent slot:
All component slots
Code examples for common slots
MembersDrawer - use the package store for the same data and actions as the default drawer:
import { useChatStore, type MembersDrawerSlotProps } from "@realtimexsco/live-chat"; const MyMembersDrawer = ({ isOpen, onOpenChange, conversationInfo }: MembersDrawerSlotProps) => { const manageGroupMembers = useChatStore((s) => s.manageGroupMembers); const participants = conversationInfo?.participants ?? []; if (!isOpen) return null; return ( <aside className="members-drawer"> <header> <h2>Members ({participants.length})</h2> <button type="button" onClick={() => onOpenChange(false)}>Close</button> </header> <ul> {participants.map((p, i) => { const id = typeof p === "string" ? p : p._id; const name = typeof p === "string" ? "User" : p.name; return <li key={`${id}-${i}`}>{name}</li>; })} </ul> </aside> );};// components={{ MembersDrawer: MyMembersDrawer }} - remove it to get the default back.ChannelDetailsDrawer
import type { ChannelDetailsDrawerSlotProps } from "@realtimexsco/live-chat"; const MyDetailsDrawer = ({ isOpen, onOpenChange, activeChannel, conversationInfo }: ChannelDetailsDrawerSlotProps) => ( <dialog open={isOpen} onClose={() => onOpenChange(false)}> <h2>{activeChannel.name}</h2> <p>{conversationInfo?.groupDescription ?? "Direct message"}</p> </dialog>);Avatar
import type { AvatarSlotProps } from "@realtimexsco/live-chat"; const MyAvatar = ({ src, name, size = "md", isOnline, showOnline }: AvatarSlotProps) => ( <div className="relative"> <img src={src || undefined} alt={name} className={`avatar avatar--${size}`} /> {showOnline && isOnline && <span className="online-dot" />} </div>);MessageItem (custom bubble) - use MessageLexicalViewer to keep rich-text formatting:
import type { MessageItemSlotProps } from "@realtimexsco/live-chat"; const MyMessageItem = ({ isSender, message, createdAt }: MessageItemSlotProps) => ( <div className={isSender ? "text-right" : "text-left"}> <div className={isSender ? "bubble-sent" : "bubble-received"}>{message}</div> <time className="text-xs opacity-60">{createdAt}</time> </div>);ChannelListItem
import type { ChannelListItemSlotProps } from "@realtimexsco/live-chat"; const MyRow = ({ channel, isActive, onSelect }: ChannelListItemSlotProps) => ( <button type="button" className={isActive ? "row-active" : "row"} onClick={() => onSelect(channel)}> {channel.name} {channel.unreadCount ? ` (${channel.unreadCount})` : ""} </button>);MessageInput
import type { ChannelMessageBoxSlotProps } from "@realtimexsco/live-chat"; const MyInput = ({ onSendMessage, replyTo, onCancelReply }: ChannelMessageBoxSlotProps) => { const [text, setText] = useState(""); return ( <div> {replyTo && <button type="button" onClick={onCancelReply}>Cancel reply</button>} <input value={text} onChange={(e) => setText(e.target.value)} /> <button type="button" onClick={() => { onSendMessage?.(text, [], replyTo); setText(""); }}> Send </button> </div> );};Wrapping package defaults
Every built-in slot component is exported, so you can wrap a default with your theme instead of rebuilding it:
import { packageDefaultComponents, type ChatComponents } from "@realtimexsco/live-chat"; function withTheme<P extends object>(Default: React.ComponentType<P>, className: string) { return (props: P) => ( <div className={className}> <Default {...props} /> </div> );} const components: ChatComponents = { MessageItem: withTheme(packageDefaultComponents.MessageItem!, "my-message"), ChannelListItem: withTheme(packageDefaultComponents.ChannelListItem!, "my-row"), Avatar: MyFullyCustomAvatar, // or DefaultChatAvatar from the package};Also exported: packageDefaultComponents (map of every slot default), DefaultChatAvatar and DefaultChannelListItem.
All classNames keys
Merged with package defaults - your classes extend the styling.
classNames={{ shell: "chat-container-root", channelListItemActive: "bg-indigo-50 dark:bg-indigo-500/10", messageBubbleSender: "rounded-2xl shadow-sm", messageBubbleReceiver: "rounded-2xl", drawer: "z-[60]",}}Events (callback props)
Event-style behavior comes through callback props and hooks rather than a global event emitter: