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.
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
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | "Messages" | Names the log. Give each thread on a page a name of its own. |
labels | MessageThreadLabels | — | Translations for the default thread name, the composer label and Send. |
...div props | React.ComponentProps<'div'> | — | Forwarded to the scroll container. |
MessageThreadItem
| Prop | Type | Default | Description |
|---|---|---|---|
messageIdrequired | string | — | Becomes the element id `message-<id>`, which `#message-<id>` addresses. |
meta | React.ReactNode | — | Rendered above the body — a name, a timestamp. |
MessageComposer
| Prop | Type | Default | Description |
|---|---|---|---|
label | React.ReactNode | "Message" | The textarea's accessible name, rendered as an sr-only label. |
value / defaultValue / onValueChange | string / string / (value: string) => void | — | Controlled or uncontrolled text. |
onSend | (value: string) => void | — | Called with the trimmed text on submit. |
error | React.ReactNode | — | The message under the textarea. Truthy also sets `aria-invalid` and links the two with `aria-describedby`. |
status | React.ReactNode | — | Announced through the role="status" note. |
pending | boolean | false | A send is in flight: the button is disabled and submit does nothing. |
clearOnSend | boolean | true | Clear the textarea after a send. Ignored while `value` is controlled. |
submitLabel | React.ReactNode | "Send" | The submit button's text. |
rows | number | 3 | Rows on the textarea. |