Skip to content
Data Display

MessageThread

A conversation, in two halves: a role=log that opens at its newest message and honours a #message- hash, and a composer built on the real Textarea with an aria-describedby error line and a role=status note. One application carried three copies of this.

Dana09:02
Thanks for sending the brief over.
You09:04
No problem — shout if anything is unclear.
Dana09:31
One question about the second milestone.
You09:33
Go ahead.

Import

import {
  MessageThread,
  MessageThreadItem,
  MessageComposer,
} from "@mikenotthepope/substrateui"

Where it opens

At the newest message. A thread that opens at the oldest one shows the reader the part they have already read. Every message that arrives after that scrolls it down again; nothing else does, so a reader who has scrolled up to find something is left alone until the conversation actually moves.

Unless the URL says otherwise. Each item carries the element id message-<messageId>, so a link to #message-42 is a link to one message — and on first paint that message is scrolled to and focused, rather than scrolled past on the way to the bottom. The focus half is the part a hand-rolled copy forgets: without it a screen reader is still reading the page the link came from. It is why MessageThreadItem carries tabIndex={-1}.

A hash that names no message in this thread is ignored, and the thread opens at the newest message as usual.

Why role=log

log is the role for a running list of messages. It announces what arrives without reading the backlog out on arrival, which is what aria-live on a plain region would do. It is also a tab stop — tabIndex={0} — because a scroll container nothing can focus is content a keyboard cannot scroll.

The composer uses Textarea

Not a bare <textarea> with Textarea's class string pasted onto it, which is what all three hand-rolled copies did — and which is why each of them drifted from the input it was imitating and none of them picked up a fix to it.

error sets aria-invalid and links the message to the textarea with aria-describedby. An empty message cannot be sent: Send stays disabled until there is something other than whitespace to send.

The status note is always there

The role="status" paragraph is in the document from the first render, empty until there is something to say. A live region inserted at the same moment as its text is a live region nothing announces — the announcement is the change, and there was nothing to change from. This is the detail a hand-rolled copy gets wrong and never finds out about, because nothing visible is different either way.

Sending that can fail

Uncontrolled, the textarea clears after a send. If your send can fail, control value — then nothing clears on its own and a rejected message is still there to fix. clearOnSend={false} keeps it while uncontrolled.

API Reference

MessageThread

PropTypeDefaultDescription
aria-labelstring"Messages"Names the log. Give each thread on a page a name of its own.
labelsMessageThreadLabels—Translations for the default thread name, the composer label and Send.
...div propsReact.ComponentProps<'div'>—Forwarded to the scroll container.

MessageThreadItem

PropTypeDefaultDescription
messageId
required
string—Becomes the element id `message-<id>`, which `#message-<id>` addresses.
metaReact.ReactNode—Rendered above the body — a name, a timestamp.

MessageComposer

PropTypeDefaultDescription
labelReact.ReactNode"Message"The textarea's accessible name, rendered as an sr-only label.
value / defaultValue / onValueChangestring / string / (value: string) => void—Controlled or uncontrolled text.
onSend(value: string) => void—Called with the trimmed text on submit.
errorReact.ReactNode—The message under the textarea. Truthy also sets `aria-invalid` and links the two with `aria-describedby`.
statusReact.ReactNode—Announced through the role="status" note.
pendingbooleanfalseA send is in flight: the button is disabled and submit does nothing.
clearOnSendbooleantrueClear the textarea after a send. Ignored while `value` is controlled.
submitLabelReact.ReactNode"Send"The submit button's text.
rowsnumber3Rows on the textarea.