Skip to content

Components · Collaboration

MessageComposer

The chat and ticket reply box: rich text with Enter to send, attachments, emoji and action slots, drafts, edit and reply modes, and a product mode switch.

@rookcom/monarch-react/editorComponent

Chat message with reply and mentions

Replying to Sam RiveraCan we move the design review to Thursday?

Message #design

Loading editor…

Usage examples

Ticket reply and internal note

Editing, attachments and disabled

Purpose

Writes and sends a message: a chat message, a thread reply, a ticket reply or an internal note. It wraps RichTextEditor with the send behaviour and the surrounding slots every conversation surface needs.

Use when

  • People send messages in a conversation, thread or ticket.
  • The same box edits a message that was already sent.
  • A reply can be public or an internal note, chosen per message.

Avoid when

  • Long-form documents or descriptions with headings: use RichTextEditor.
  • A one-line comment with no formatting: a Textarea with a button loads no editor.
  • Email campaigns with templates and merge fields: that builder is product-specific.

Design principles

  • Sending is the primary action: one obvious button, one key (Enter, or ⌘/Ctrl+Enter where messages are long), and the text is never lost if sending fails.
  • The product owns the conversation: uploads, emoji, mentions search, drafts storage, typing presence and modes are callbacks and slots.
  • An internal note must never look like a public reply: the note tone changes the box surface, not just a label.
  • Compact by default: formatting sits on one row with the actions, so the box is small until someone writes more.

API reference

import { MESSAGE_COMPOSER_TOOLBAR, MessageComposer } from '@rookcom/monarch-react/editor';
ExportKindRenderingRoot class
MESSAGE_COMPOSER_TOOLBARutilityclient–
MessageComposercomponentclient.mn-composer-

Renders .mn-message-composer, containing the mode row (__modes), the reply or edit context (__context), the attachment row (__attachments), a RichTextEditor (__editor, toolbar below the text with the actions at its end) and a footer (__footer) with the key hint or disabled reason and the draft status.

editorRef reaches the underlying RichTextEditor handle (focus, insertText, clear, getHTML…).

Exports MESSAGE_COMPOSER_TOOLBAR, the default compact toolbar.

Generated from the TypeScript declarations of packages/react/src/components/collaboration/message-composer.

Properties

onSend(message) receives { html, text, json, mode }. Return a promise for the pending state: the box clears when it resolves and keeps the text when it rejects. Or control the state with sending.

value and defaultValue are HTML (the composer always uses format="html"); restore a saved draft with defaultValue.

onDraftChange(html) runs after typing pauses for draftDebounceMs, on unmount if a save is still waiting, and with "" after a successful send. Show the result with draftStatus.

modes is product data: each mode has a value, label, optional tone: "note", placeholder and send label. The chosen mode is passed to onSend.

MessageComposer

The chat and ticket reply box: a compact RichTextEditor with Enter to send, an attachment row, emoji and action slots, a send button with a pending state, draft persistence, edit and reply-to modes, typing callbacks and a product-supplied mode switch (public reply / internal note).

PropTypeDescription
label requiredstringAccessible name of the text box, e.g. "Message #design" or "Reply to customer".
onSend required(message: MessageComposerMessage) => unknownCalled with the message. Return a promise to show the pending state; the box clears when it resolves and keeps the text when it rejects.
actionsReactNodeExtra controls before the send button (schedule, macros, AI assist).
attachmentsReactNodeThe attachment row (uploads in progress, chips), rendered above the text.
autoFocusboolean
childrenReactNode
classNamestring
defaultModestring
defaultValuestringInitial HTML (a restored draft, or the message being edited).
disabledboolean
disabledReasonReactNodeWhy the composer is disabled, shown under it and linked to the text box.
draftDebounceMsnumberPause before `onDraftChange`. Default 600 ms.
draftStatusMessageComposerDraftStatusShown under the box: the product's draft save state.
editingbooleanEdit an existing message: "Save" label, Escape or Cancel calls `onCancelEdit`.
editorRefRef<RichTextEditorHandle>The underlying editor's handle (focus, insertText, clear, …).
emoji((insert: (text: string) => void) => ReactNode)Renders the product's emoji picker; call `insert` with the chosen emoji.
fileAcceptstring`accept` for the attach button's file picker.
hasAttachmentsbooleanAllows sending with no text, because the attachment row has files.
idstring
maxLengthnumber
mentionsRichTextMentionOptions
messagesPartial<MessageComposerMessages>
modestring
modeAsideReactNodeExtra content beside the mode switch, e.g. the audience ("To: customer@…").
modesreadonly MessageComposerMode[]Mode switch (public reply / internal note). Product-supplied.
onCancelEdit(() => void)
onDismissReply(() => void)
onDraftChange((html: string) => void)Persists the draft. Called after typing pauses, and with `''` after sending.
onEditLast(() => void)Arrow Up in an empty box, e.g. to edit your last message.
onFilesRichTextFileHandlerReceives pasted, dropped and picked files. Adds the attach button.
onModeChange((mode: string) => void)
onTyping(() => void)Someone is typing. Called at most once per `typingIntervalMs` while the text changes.
onTypingStop(() => void)Typing stopped: the box was emptied, sent or left.
onValueChange((html: string) => void)
placeholderstring
replyToMessageComposerReplyToThe message being answered, shown above the text.
sendingbooleanControlled pending state, for products that track sending themselves.
sendLabelstringSend button text. Default "Send", or "Save" while `editing`.
sendOn"enter" | "mod-enter"`enter` (default): Enter sends, Shift+Enter adds a line. `mod-enter`: ⌘/Ctrl+Enter sends.
showCharacterCountboolean
showHintbooleanShow the send-key hint. Default true.
toolbarfalse | readonly RichTextToolbarItem[]Formatting controls, or `false` for none. Default `MESSAGE_COMPOSER_TOOLBAR`.
typingIntervalMsnumberDefault 3000 ms.
valuestringControlled HTML.

Variants

PropValuesDescription
sendOn'enter''mod-enter'Chat-style (Enter sends, Shift+Enter adds a line) or long-form (Enter adds a line, ⌘/Ctrl+Enter sends).
modes[].tone'default''note'A note gives the box the warning surface, so private notes are unmistakable.
editing'false''true'Edit an existing message: "Editing message" context, Save and Cancel, Escape cancels.

States

StateExposed asBehaviour
Emptydisabled on the send buttonSend is disabled until there is text, or hasAttachments is set.
Sendingaria-busy on the send buttonThe button shows a spinner and "Sending…"; Enter does nothing until it settles.
Replying.mn-message-composer__contextThe quoted author and excerpt above the text, with a dismiss button.
Editingdata-editingEditing context, Save and Cancel; the text is focused.
Internal notedata-tone="note"Warning border and surface on the text box.
Disableddata-disabledThe box, toolbar and actions are inert; disabledReason replaces the key hint and is linked to the text box.
Draft statusdata-status on the status line"Saving draft…", "Draft saved" or "Draft not saved" (danger colour), in a status region.

Interaction behaviour

  • Enter sends (or ⌘/Ctrl+Enter with sendOn="mod-enter"); ⌘/Ctrl+Enter always sends. Shift+Enter adds a line break. Inside a code block Enter adds a line, so code can be typed naturally.
  • While a mention list is open, Enter picks the mention instead of sending.
  • The send button and the keys do nothing while sending, so a message is never sent twice.
  • Files pasted or dropped into the text, and files from the attach button, go to onFiles; the product renders them in attachments.
  • emoji(insert) renders the product’s picker; insert puts the emoji at the caret.
  • Arrow Up in an empty box calls onEditLast (edit your last message).
  • onTyping fires at most once per typingIntervalMs while the text changes; onTypingStop fires when the box is emptied, sent or left.
  • Changing mode keeps the text; the placeholder and send label follow the mode.

Keyboard support

KeysAction
EnterSends (default), or adds a line with sendOn="mod-enter".
⌘/Ctrl+EnterSends.
Shift+EnterAdds a line break.
Escape (editing)Cancels the edit.
ArrowUp (empty box)Calls onEditLast.
ArrowLeft / ArrowRight (mode switch)Changes the mode.

All RichTextEditor keys apply: formatting shortcuts, the toolbar’s arrow keys and mention suggestions.

Accessibility

Role
textbox (multi-line) with a formatting toolbar; radiogroup for modes
Accessible name
label names the text box. The mode switch is named by messages.modes (default "Message type").
  • The key hint ("Enter to send, Shift+Enter for a new line") is linked to the text box with aria-describedby, so the send key is announced.
  • A disabled composer links its reason to the text box, so people know why they cannot write.
  • Draft status is a polite status region.
  • The send button keeps focus while sending (aria-busy, aria-disabled), so keyboard users are not dropped.
  • Attach, dismiss-reply and the product’s emoji slot are labelled buttons in the Tab order after the formatting toolbar.

Responsive behaviour

  • The toolbar and actions share one row and wrap when the composer is narrow; the send button never shrinks.
  • Below 22rem of composer width (a chat dock, a thread panel) the key hint is hidden; it stays available to screen readers through the text box description.
  • The text grows from one line to 15rem, then scrolls.

Theme support

  • Inherits RichTextEditor’s tokens; the note tone uses --mn-color-warning-subtle and --mn-color-warning-border, the reply context --mn-color-bg-subtle with an --mn-color-accent-border edge.
  • Light, dark, high-contrast and product accents apply through tokens; reduced motion removes transitions.

Do and don't

Do

  • Return the send promise so the composer can show the pending state and keep the text on failure.
  • Use sendOn="mod-enter" where replies are long (tickets, email-like replies), and the default where they are short (chat).
  • Give internal notes tone: "note" and a distinct send label ("Add note").
  • Persist drafts per conversation with onDraftChange and restore them with defaultValue.

Don't

  • Do not clear the box yourself before the send succeeds; return the promise instead.
  • Do not hide why a composer is disabled; pass disabledReason.
  • Do not put headings or checklists in a chat composer; the default toolbar leaves them out on purpose.

Dependencies

KindDepends on
npm packagesreact
Monarch componentsButton, SegmentedControl, RichTextEditor

Button, IconButton and SegmentedControl for the actions and modes; Tiptap comes through RichTextEditor, so the same optional peers apply.

Adds about 3 KB min+gzip on top of RichTextEditor (127 KB for the entry with both).

Testing coverage

  • Enter sends, Shift+Enter adds a line, the box clears; mod-enter mode; Enter in a code block adds a line.
  • Send disabled while empty; pending state for a promise with no double send; text kept when sending fails.
  • Sending only attachments with hasAttachments; attach button and file picker reach onFiles; emoji slot inserts at the caret.
  • Draft debounce and the empty draft after sending; typing callbacks throttled and stopped on send.
  • Editing (Save, Escape and Cancel), reply-to preview and dismiss, onEditLast on Arrow Up.
  • Disabled with a linked reason; modes switch tone, placeholder and send label and reach onSend; controlled value.
CheckCovered
Axe in jsdomYes
Server renderingYes
Browser suitesVisual baselines: /__gallery/message-composer; Cross-browser axe: /components/message-composer

Tests: packages/react/src/components/collaboration/message-composer