Source: https://monarchui.dev/components/message-row/

Components · Collaboration

# MessageRow

One message: avatar, author, time and body, with edited, pending and failed states, reactions, replies and actions.

@rookcom/monarch-react/collaboration

Component

## Reactions, replies and actions

## Usage examples

### Grouping, edited and states

### Internal note and system line

## 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

```tsx
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 `onReactionToggle` is set, otherwise read-only.
- The reply count calls `onOpenThread`.
- Retry and Discard call `onRetry` and `onDiscard`; 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-pressed` and a spoken name such as “thumbs up, 3”; pass `label` for each emoji.
- The time is a `<time datetime>` with the full date in `title`.
- 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 notes `warning-subtle` with a `warning-border` edge, highlight `accent-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 `grouped` for you.
- Give every reaction a spoken `label` and a `title` listing who reacted.
- Put the audience (“Internal”, “Reply to customer”) in `tags` as 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](https://monarchui.dev/components/message-timeline/): The scrolling conversation that groups and separates rows.
- [MessageComposer](https://monarchui.dev/components/message-composer/): Writing the next message (planned).
- [ActivityTimeline](https://monarchui.dev/components/activity-timeline/): System events with actor, verb and object.
- [Avatar](https://monarchui.dev/components/avatar/): The author picture.
- [RelativeTime](https://monarchui.dev/components/relative-time/): The timestamp.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `react` |
| Monarch components | [Avatar](https://monarchui.dev/components/avatar/), [RelativeTime](https://monarchui.dev/components/relative-time/) |

## 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 `onShowEdits` is set.
- Pending status, failed alert with Retry and Discard, default failure text.
- Reaction toggles with `aria-pressed` and 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`

## All example code

### Reactions, replies and actions

```tsx
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 />
```

### Grouping, edited and states

```tsx
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

```tsx
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>
```
