Components · Collaboration
MessageRow
One message: avatar, author, time and body, with edited, pending and failed states, reactions, replies and actions.
Reactions, replies and actions
import { useState } from 'react';
import { IconButton } from '@rookcom/monarch-react';
import { MessageRow, type MessageReaction } from '@rookcom/monarch-react/collaboration';
// MoreIcon, PencilIcon: demo icons from the docs site; use your own.
const NOW = '2026-03-02T12:00:00Z';
function ReactionsDemo() {
const [reactions, setReactions] = useState<MessageReaction[]>([
{ emoji: '👍', label: 'thumbs up', count: 3, reacted: true, title: 'You, Ben and Chloé' },
{ emoji: '🎉', label: 'party popper', count: 1 },
]);
const toggle = (emoji: string) =>
setReactions((list) =>
list.map((r) =>
r.emoji === emoji
? { ...r, reacted: !r.reacted, count: r.count + (r.reacted ? -1 : 1) }
: r,
),
);
return (
<div style={{ width: '100%' }}>
<MessageRow
author={{ name: 'Ana Lopez' }}
time="2026-03-02T11:52:00Z"
now={NOW}
reactions={reactions}
onReactionToggle={toggle}
replyCount={4}
onOpenThread={() => {}}
actions={
<>
<IconButton size="xs" label="Edit message" icon={<PencilIcon />} />
<IconButton size="xs" label="More actions" icon={<MoreIcon />} />
</>
}
>
<p>The release notes are ready for review. Comments by Thursday, please.</p>
</MessageRow>
</div>
);
}
<ReactionsDemo />Usage examples
Grouping, edited and states
import { MessageRow } from '@rookcom/monarch-react/collaboration';
const NOW = '2026-03-02T12:00:00Z';
<div style={{ width: '100%' }}>
<MessageRow author={{ name: 'Ben Okafor' }} time="2026-03-02T11:40:00Z" now={NOW}>
Draft is in the shared folder.
</MessageRow>
<MessageRow
author={{ name: 'Ben Okafor' }}
time="2026-03-02T11:41:00Z"
now={NOW}
grouped
edited
>
Updated the second section too.
</MessageRow>
<MessageRow
author={{ name: 'Ben Okafor' }}
time="2026-03-02T11:58:00Z"
now={NOW}
grouped
status="pending"
>
Sending this one now…
</MessageRow>
<MessageRow
author={{ name: 'Ben Okafor' }}
time="2026-03-02T11:59:00Z"
now={NOW}
grouped
status="failed"
error="No connection. Not sent."
onRetry={() => {}}
onDiscard={() => {}}
>
And the summary slide.
</MessageRow>
<MessageRow
author={{ name: 'Chloé Martin' }}
time="2026-03-02T10:00:00Z"
now={NOW}
deleted
/>
</div>Internal note and system line
import { Badge } from '@rookcom/monarch-react';
import { MessageRow } from '@rookcom/monarch-react/collaboration';
const NOW = '2026-03-02T12:00:00Z';
<div style={{ width: '100%' }}>
<MessageRow
author={{ name: 'Dev Patel' }}
time="2026-03-02T09:10:00Z"
now={NOW}
variant="internal"
tags={
<Badge tone="warning" size="sm">
Internal
</Badge>
}
>
Customer is on the legacy plan. Check billing before replying.
</MessageRow>
<MessageRow
author={{ name: 'System' }}
time="2026-03-02T09:12:00Z"
now={NOW}
variant="system"
>
Dev Patel assigned this request to the Billing team
</MessageRow>
</div>Purpose
Shows one message in a conversation, comment thread or ticket reply list, with the states real messaging needs: grouped follow-ups, sending, failed with retry, edited, deleted, internal notes and system lines.
Use when
- Chat channels, direct messages and threads.
- Ticket and request conversations, including internal notes.
- Comment threads on records, inside a MessageTimeline or any list.
Avoid when
- A feed of system events with verbs and objects ("Ana changed the status"): use ActivityTimeline.
- A notification that leads elsewhere: use NotificationCenter.
Design principles
- The body is the product’s: Monarch lays out the row and never parses or sanitises message content.
- Every message is named by its author and time, even when grouped, so a screen reader never hears an anonymous line.
- Actions appear on hover and whenever focus is inside the row, so the keyboard reaches them without a hidden mode; touch screens always show them.
- Failure is recoverable in place: the error, Retry and Discard sit under the message that failed.
API reference
import { MessageRow } from '@rookcom/monarch-react/collaboration';| Export | Kind | Rendering | Root class |
|---|---|---|---|
MessageRow | component | client | .mn-message-row |
The ref is forwarded to the root <article>.
messages translates every built-in string; replies and reaction are functions for plurals.
Generated from the TypeScript declarations of packages/react/src/components/collaboration/message-row.
Properties
MessageRow
One message in a conversation: avatar, author, time and body, with edited, pending, failed and deleted states, reactions, a thread reply count and per-message actions. Consecutive messages from one author are shown `grouped`.
| Prop | Type | Description |
|---|---|---|
author required | MessageAuthor | |
time required | DateInput | When the message was sent. A Date, epoch milliseconds or an ISO 8601 string. |
actions | ReactNode | Per-message controls (react, reply, edit, more). Rendered in a group that appears on hover and whenever focus is inside the row, so it is reachable with Tab; always visible on touch screens. |
avatar | ReactNode | Replaces the avatar, for example an app or bot glyph. |
children | ReactNode | The message body: text, rich content, attachments. Monarch does not sanitise HTML; the product does. |
className | string | |
deleted | boolean | Replaces the body with the deleted placeholder and hides reactions and actions. |
edited | boolean | Shows the edited marker. |
error | ReactNode | Why sending failed. Default `messages.failed`. |
footer | ReactNode | Content below the body: attachments, link previews, read receipts. |
grouped | boolean | A follow-up from the same author: hides the avatar and header and shows the time in the gutter on hover and focus. The author name stays in the accessible name. MessageTimeline sets this for you. |
highlighted | boolean | Briefly highlights the row, for example after a deep link. |
id | string | |
locale | string | |
messages | Partial<MessageRowMessages> | Translations of the built-in text. |
now | DateInput | Reference time for the relative timestamp. Pass it for identical server and client output. |
onDiscard | (() => void) | |
onOpenThread | (() => void) | |
onReactionToggle | ((emoji: string) => void) | Called with the emoji when a reaction chip is pressed. Without it the chips are read-only. |
onRetry | (() => void) | |
onShowEdits | (() => void) | Makes the edited marker a button that opens the edit history. |
reactionAction | ReactNode | Extra control after the reaction chips, typically an "Add reaction" button. |
reactions | readonly MessageReaction[] | |
replyCount | number | Thread reply count. Shown as a button when `onOpenThread` is set. |
status | MessageRowStatus | Delivery state. `pending` dims the body; `failed` shows the error with Retry and Discard. |
tags | ReactNode | Small tags after the author name: audience ("Internal"), role ("Guest", "App"), pinned. |
timeDisplay | "relative" | "clock" | `relative` ("5 minutes ago", default) or `clock` ("14:05"). Both render a `<time datetime>` with the full date in `title`. |
timeZone | string | |
variant | MessageRowVariant | `internal` tints the row for notes only teammates can see (with a tag in `tags`); `system` is a centred, compact line for server-written events. |
Variants
| Prop | Values | Description |
|---|---|---|
variant | 'default''internal''system' | internal tints the row and draws a warning edge for teammate-only notes (add an audience tag in tags); system is a centred one-line event. |
grouped | 'false''true' | Follow-up from the same author: no avatar or header, the time appears in the gutter on hover and focus. |
timeDisplay | 'relative''clock' | “5 minutes ago” (RelativeTime) or “14:05”. Both carry the full date in title. |
States
| State | Exposed as | Behaviour |
|---|---|---|
| Pending | data-status="pending" | Body muted, “Sending…” in a status region, actions hidden, aria-busy. |
| Failed | data-status="failed" | The error (or “Not sent”) in an alert, with Retry and Discard when their callbacks are given. |
| Edited | – | “(edited)” after the body; a button when onShowEdits opens the history. |
| Deleted | data-deleted | “This message was deleted.” replaces the body; reactions, footer and actions are dropped. |
| Highlighted | data-highlighted | Accent tint for a deep-linked or searched message. |
Interaction behaviour
- Reaction chips are toggle buttons when
onReactionToggleis set, otherwise read-only. - The reply count calls
onOpenThread. - Retry and Discard call
onRetryandonDiscard; the product resends or removes the message. - The actions group becomes visible on pointer hover and whenever focus enters the row.
Keyboard support
| Keys | Action |
|---|---|
| Tab | Moves through the row’s controls: edited marker, reactions, replies, then the actions. |
| Enter / Space | Activates the focused reaction, reply count or action. |
The row itself is not a tab stop; only its controls are.
Accessibility
- Role
- article
- Accessible name
- Labelled by the author name and the time (
aria-labelledby), also when grouped.
- Grouped rows keep a visually hidden author name.
- Reaction chips use
aria-pressedand a spoken name such as “thumbs up, 3”; passlabelfor each emoji. - The time is a
<time datetime>with the full date intitle. - Pending text is a polite status; a failure is an alert so the sender hears it.
- Actions sit in a named group (“Message actions”).
Responsive behaviour
- Header items wrap under the name on narrow screens; long words in the body break.
- On touch screens (
hover: none) the actions stay visible at the end of the row and the grouped time is shown.
Theme support
- Hover and focus use
bg-subtle, internal noteswarning-subtlewith awarning-borderedge, highlightaccent-subtle. - Pressed reactions use the accent tokens; failed text
danger-strong. Forced colours outline the highlighted row.
Do and don't
Do
- Render rows inside MessageTimeline, which sets
groupedfor you. - Give every reaction a spoken
labeland atitlelisting who reacted. - Put the audience (“Internal”, “Reply to customer”) in
tagsas text, not colour alone.
Don't
- Do not put unsanitised HTML in the body; sanitise in the product.
- Do not hide important actions only in the hover group; keep primary actions such as Reply reachable elsewhere too on small screens.
- Do not use the system variant for user-written content.
Related components
- MessageTimeline: The scrolling conversation that groups and separates rows.
- MessageComposer: Writing the next message (planned).
- ActivityTimeline: System events with actor, verb and object.
- Avatar: The author picture.
- RelativeTime: The timestamp.
Dependencies
| Kind | Depends on |
|---|---|
| npm packages | react |
| Monarch components | Avatar, RelativeTime |
Testing coverage
- Names the article by author and time, also when grouped, and renders a
<time datetime>with the full date. - Edited marker, and the edit-history button when
onShowEditsis set. - Pending status, failed alert with Retry and Discard, default failure text.
- Reaction toggles with
aria-pressedand spoken counts; read-only reactions. - Thread count, keyboard-reachable actions group, deleted placeholder.
- Internal, highlighted, system and clock variants; translated text; invalid time.
| Check | Covered |
|---|---|
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery/message-row; Cross-browser axe: /components/message-row |
Tests: packages/react/src/components/collaboration/message-row