Skip to content

MessageComposer

<caos-message-composer>

the box a message is written in, and the control that sends it

May be placed on a record page, an app’s home page and a page inside an app.

Name Attribute Description Type Default Required
placeholder placeholder, markup only The grey line in the empty box. Worth setting to name the conversation — “Message #deploys” tells somebody where what they type is about to go, which a generic line does not. text Write a message… Optional
disabled disabled, markup only Nothing can be typed or sent. For a conversation that is closed, an outage, or somebody who may read but not write. The box says so by being visibly unavailable rather than by refusing after somebody has written a message. boolean false Optional
sendLabel send-label, markup only What the send control says, for a surface whose word for this is not “send”. text Send Optional
attach attach, markup only Offer an attach control. Chosen files are REPORTED through the attach event and are not uploaded, held or attached to anything by this component — it does not know where your files go. boolean false Optional
dictate dictate, markup only Offer a dictate control, IF the browser has speech recognition. Asking for it in a browser that does not draws nothing rather than a control that fails when pressed. What is recognised is appended to what was already typed. boolean false Optional
maxHeight max-height, markup only How tall the box may grow, in pixels, before it scrolls instead. A box with no ceiling pushes the conversation it belongs to off the screen. number 160 Optional
mentions mentions, markup only Typing “@” opens a list above the box. The box does not know who exists: it asks through the mention-query event, and the surface answers with answerMentions. Without this, “@” is an ordinary character and nothing is asked. boolean false Optional
mentionCandidates property only The answer to the question being asked RIGHT NOW — { kind, ref, label, hint? } entries, in the order they should read. For a surface that answers synchronously. Anything that fetches must call answerMentions instead and name the query it is answering, or a slow answer to an older question will be shown as the answer to the current one. json empty list Optional
Name When it fires What it carries
send Somebody pressed Enter or the send control with something other than whitespace in the box. The box has already cleared itself and taken focus back by the time this is heard — sending is the surface’s job, and if it fails the surface says so. { body: string }
draft On every keystroke, and once more with an empty value after a send. Whether a draft is stored, where and for how long is the surface’s decision; this component keeps nothing. { value: string }
mention-query The mention being typed changed — including to null, which means one is no longer being typed. Null is not “nothing matched”: it is what tells the surface to abandon a lookup still in flight, and a surface that ignores it will answer a question nobody is asking any more. An empty string is a real query — a bare “@” asks who is here. { query: string
mention Somebody chose from the list and the mention has been written into the box. Raised as well as draft, because the CHOICE is what lets a surface keep the label it has already resolved instead of asking again. { kind, ref, label, hint? }
attach Somebody chose one or more files. Nothing has been uploaded. The chooser is cleared afterwards so that choosing the same file twice in a row is still reported — a file input otherwise reports only a CHANGE, and the second attempt would be silent. { files: File[] }

Nothing goes inside this piece.

Design token What it controls
--caos-color-text What is being typed, and the labels on the controls.
--caos-color-text-muted The box when nothing may be typed into it.
--caos-color-text-ghost A control that is not available.
--caos-color-border The box’s edge, the controls’ edges and the rule above the whole thing.
--caos-color-surface Behind the composer, so it reads as a band under the conversation.
--caos-color-surface-2 Inside the box and behind the controls.
--caos-color-accent-fill The send control once there is something to send, and the dictate control while it is listening.
--caos-color-accent-contrast The text drawn on that fill.
--caos-font The typeface of the message and the controls.
--caos-text-body The size of what is being typed.
--caos-text-label The size of the control labels.
--caos-space-1 The gap between the controls.
--caos-space-2 The inside of the box, and the gap between the box and the controls.
--caos-space-3 The inset from the edges of the conversation, and how far in the mention list sits.
--caos-radius-sm The roundness of the box and the controls.
--caos-focus-ring The ring on the box and on each control.
Name Description Arguments
submit() Send what is in the box, as pressing Enter would. Returns what was sent, or null when the box was empty or unavailable — so a caller can tell “sent” from “there was nothing to send”. not stated
focus() Put the cursor in the box. not stated
answerMentions() Answer one mention-query, naming the query being answered. Returns whether the answer was used — an answer to a question nobody is asking any more is dropped. This is the path an asynchronous lookup must take: two lookups in flight can finish in either order, and an answer that cannot say which question it answers cannot be checked against the one being asked. query: text, candidates: json
Name Type What it tells you
value text What is in the box. Readable and settable — setting it is how a stored draft comes back in, and it re-measures the box’s height.
mentionsEnabled boolean Whether this box is offering mentions at all.
pendingMentionQuery text, or null The query this box is waiting on an answer for, or null when it is not waiting on one. What a surface passes back to answerMentions.
Part Which piece of it When it is there
::part(composer) the whole band Always
::part(input) the growing text box Always
::part(actions) the run of controls beside it Always
::part(send) the send control Always
::part(attach) the attach control Always
::part(dictate) the dictate control Always
::part(mentions) the list of people offered while a mention is being typed mentions are offered

Somebody is writing prose that is SENT — a message, a reply, a comment. A value being entered on a record is a field, and belongs to the field renderer with everything a field brings: a label, validation, a save.

  • TextField — The text is a field’s value. It gets a label, validation and the save behaviour of the record it belongs to, none of which a message has.
  • FieldRenderer — The writing is a rich Text field on a record, where the result is stored formatting on that record. A message carries a small markdown and is typed, not styled.
  • ActionBar — What is wanted is the row of actions, with no text being written at all.

While mentions are offered the box is the COMBOBOX of a combobox-and-listbox pair: it carries role="combobox", aria-expanded, aria-controls and aria-activedescendant, and the list beside it never takes focus. That is what lets the arrows, Enter and Escape reach the list while every character still goes into the box. Escape closes the list without deleting what was typed, and does not reopen it until the caret leaves that mention. None of those attributes is set when mentions are not offered, so a plain composer is a plain text box. The box is a plain <textarea>, so everything a browser and an assistive technology already know about typing into one holds: selection, dictation at the operating-system level, a text cursor, and an input method mid-character. Enter is only intercepted when NO modifier is held and the input method is not composing, which is what stops it from stealing the key that finishes a character. The send control is genuinely disabled while there is nothing to send, so it is skipped rather than offered and refused, and the dictate control carries aria-pressed while it is listening. Left to the author: NAMING the box. It has a placeholder and no label, and a placeholder is not a name — set aria-label on the element, or label it from the conversation’s own heading.

Named for the conversation it sends into.

Cannot be shown as a page placement: is given aria-label, which is set on the element rather than declared as an input. A stored placement carries only a component’s own inputs, so a page copying this would place the piece without it.

<caos-message-composer placeholder="Message #deploys" aria-label="Message #deploys"></caos-message-composer>

Both are opt-in. Dictate draws nothing at all in a browser without speech recognition, rather than drawing a control that fails when pressed.

Cannot be shown as a page placement: is given aria-label, which is set on the element rather than declared as an input. A stored placement carries only a component’s own inputs, so a page copying this would place the piece without it.

<caos-message-composer placeholder="Message #deploys" attach dictate aria-label="Message #deploys"></caos-message-composer>

Typing “@” asks the surface who is here, through the mention-query event. This preview asks and is answered by nothing, so no list opens — which is also what a real surface shows while it is still looking. A list appears when mentionCandidates is set, and an empty list set there says “nobody by that name” rather than staying shut.

Cannot be shown as a page placement: is given aria-label, which is set on the element rather than declared as an input. A stored placement carries only a component’s own inputs, so a page copying this would place the piece without it.

<caos-message-composer placeholder="Message #deploys" mentions aria-label="Message #deploys"></caos-message-composer>

An outage, an archived conversation, or somebody who may read but not write.

Cannot be shown as a page placement: is given aria-label, which is set on the element rather than declared as an input. A stored placement carries only a component’s own inputs, so a page copying this would place the piece without it.

<caos-message-composer placeholder="This conversation is closed" disabled aria-label="Closed conversation"></caos-message-composer>