Composer
- Stable
- WCAG 2.2 evidence
- RTL
The message box under a conversation: a growing textarea
with attach, emoji and voice buttons, a Send button, a "replying to" bar, attachments and a voice recording row. Built on React Aria TextField + TextArea and Buttons. The textarea is named by label, aria-label or
aria-labelledby (never the placeholder) and described by the helper text, the reply bar, the error and the keyboard
hint (WCAG 1.3.1, 3.3.2, 4.1.2). Enter sends and Shift+Enter adds a new line; IME composition never sends; Escape
cancels a reply (2.1.1). Every button has an accessible name and a 24 px or larger target (2.5.8, 4.1.2); Send is
disabled while there is nothing to send and focus returns to the textarea after sending. Errors show an icon and text
and are announced (1.4.1, 3.3.1, 4.1.3); recording is announced. Paste, autofill (autoComplete) and spell checking
are never blocked (3.3.7, 3.3.8). The field wraps and grows instead of scrolling sideways (1.4.10, 1.4.12). Controlled (value + onChange) or uncontrolled (defaultValue; cleared after sending). Consumer duties: the label and every translated button name; an error message that says what to do; sendOnEnter={false}
where Enter must add lines; announcing sent and received messages through the conversation's role="log".
import { Composer } from "@nexera-ui/react";- 5
- examples
- 51
- props
- 8
- live controls
- 1
- platform
- 14
- WCAG criteria
- 4
- blocks use it
Try every prop. Copy the code.
Change the props and the code updates. Check light and dark, LTR and RTL, and phone width.
import { Composer } from "@nexera-ui/react";
<Composer />Examples 4
The same examples as Storybook, rendered live. Open Code to copy one.
Interactive
A working composer: reply, attach a file, record a voice note and send.
import { useEffect, useState } from "react";
import { Badge, Composer } from "@nexera-ui/react";
const noop = () => undefined;
export function Interactive() {
const [replying, setReplying] = useState(true);
const [files, setFiles] = useState<string[]>([]);
const [recording, setRecording] = useState(false);
const [seconds, setSeconds] = useState(0);
const [sent, setSent] = useState<string[]>([]);
useEffect(() => {
if (!recording) return;
const id = setInterval(() => {
setSeconds((value) => value + 1);
}, 1000);
return () => {
clearInterval(id);
};
}, [recording]);
return (
<div className="flex w-full max-w-xl flex-col gap-3">
<ul role="log" aria-label="Sent messages" className="m-0 flex list-none flex-col gap-1 p-0">
{sent.map((message, index) => (
<li key={index} className="text-body-default text-primary whitespace-pre-wrap">
{message}
</li>
))}
</ul>
<Composer
aria-label="Message Sara Malik"
placeholder="Message Sara Malik"
onAttach={noop}
onEmoji={noop}
onRecord={noop}
{...(replying
? { replyAuthor: "Sara Malik", replyTo: "Leave policy for public holidays" }
: {})}
onCancelReply={() => {
setReplying(false);
}}
onAttach={() => {
setFiles((list) => [...list, `Document ${String(list.length + 1)}.pdf`]);
}}
attachments={
files.length > 0 ? (
<>
{files.map((file) => (
<Badge
key={file}
tone="neutral"
dot={false}
label={file}
removeLabel={`Remove ${file}`}
onRemove={() => {
setFiles((list) => list.filter((item) => item !== file));
}}
/>
))}
</>
) : null
}
onRecord={() => {
setSeconds(0);
setRecording(true);
}}
isRecording={recording}
recordingTime={`0:${String(seconds).padStart(2, "0")}`}
onCancelRecording={() => {
setRecording(false);
}}
onSend={(message) => {
setSent((list) => [
...list,
recording ? `Voice message (0:${String(seconds).padStart(2, "0")})` : message,
...files.map((file) => `Attachment: ${file}`),
]);
setRecording(false);
setReplying(false);
setFiles([]);
}}
/>
</div>
);
}
Label and error
A visible label, helper text and a send error (icon and text, announced when it appears).
import { Composer } from "@nexera-ui/react";
const noop = () => undefined;
export function LabelAndError() {
return (
<Composer
label="Reply to the people team"
description="Visible to everyone in #people-ops."
isInvalid
errorMessage="Message not sent. Check your connection and try again."
defaultValue="Approved. Enjoy the long weekend."
onAttach={noop}
/>
);
}
Long message
A long message wraps and the field grows up to about six lines, then scrolls inside the field.
import { Composer } from "@nexera-ui/react";
const noop = () => undefined;
<div className="w-full max-w-xs">
<Composer
aria-label="Message Sara Malik"
placeholder="Message Sara Malik"
onAttach={noop}
onEmoji={noop}
onRecord={noop}
defaultValue="Hi Sara,\nCould you approve my leave for Friday 12 March? I have handed the payroll check over to Bilal, and the onboarding documents for the two new starters are in the shared folder https://intranet.example.com/hr/onboarding/2026/march.\nThanks!"
/>
</div>;
Right to left
Right-to-left: attach on the right, Send on the left; the reply bar's accent mirrors.
import { Composer } from "@nexera-ui/react";
const noop = () => undefined;
<Composer
aria-label="رسالة إلى سارة مالك"
placeholder="رسالة إلى سارة مالك"
onAttach={noop}
onEmoji={noop}
onRecord={noop}
replyAuthor="سارة مالك"
replyTo="سياسة الإجازات في العطل الرسمية"
onCancelReply={noop}
sendLabel="إرسال"
attachLabel="إرفاق ملف"
emojiLabel="إضافة رمز تعبيري"
recordLabel="تسجيل رسالة صوتية"
cancelReplyLabel="إلغاء الرد"
replyLabel="رد على"
keyboardHint="Enter للإرسال، Shift+Enter لسطر جديد."
/>;
Props 51
Press "Try it" on a card to load that prop into the playground.
51 props shown
placeholderNexerastringExample text while the field is empty. Never the only
name: give the composer a label or aria-label too.
descriptionNexeraReactNodeHelper text under the row, linked with aria-describedby (WCAG 3.3.2).
errorMessageNexeraReactNode | ((validation: ValidationResult) => ReactNode)Error text under the row (for example "Message not sent. Check your connection."), shown with an icon while
isInvalid is true, linked with aria-describedby and announced politely when it appears (WCAG 3.3.1, 4.1.3).
onSendNexera(message: string) => voidCalled with the message when the user sends it: the Send button, or Enter when sendOnEnter. Not called for an
empty (whitespace-only) message unless attachments are shown. While isRecording, Send calls it with the typed text
(usually empty): send the recording then. Uncontrolled composers clear themselves after sending.
sendLabelNexerastringAccessible name of the Send button. Translate it.
sendOnEnterNexerabooleanEnter sends and Shift+Enter inserts a line break (the chat convention). While an input method editor is composing
(Chinese, Japanese, Korean), Enter confirms the composition and never sends. Set false where Enter should always add
a line break (for example long-form replies, or on touch keyboards), so only the Send button sends.
keyboardHintNexerastringVisually hidden instruction linked to the textarea with aria-describedby while sendOnEnter is true, so screen reader
users know how Enter behaves before they type (WCAG 3.3.2). Translate it; pass "" to leave it out.
onAttachNexera() => voidShows the attach button and is called when it is pressed, for example to open a file picker.
attachLabelNexerastringAccessible name of the attach button. Translate it.
onEmojiNexera() => voidShows the emoji button in the field and is called when it is pressed.
emojiLabelNexerastringAccessible name of the emoji button. Translate it.
onRecordNexera() => voidShows the voice button in the field and is called when it is pressed: start
recording and set isRecording.
recordLabelNexerastringAccessible name of the voice button. Translate it.
attachmentsNexeraReactNodeAttached files shown above the row, for example removable Badges. While present, Send is enabled
even without text.
replyToNexeraReactNodeThe message being replied to: shows the reply bar above the row. It is linked
to the textarea with aria-describedby, so it is read with the field.
replyAuthorNexeraReactNodeAuthor of the message being replied to, shown above replyTo.
replyLabelNexerastringVisually hidden text that starts the reply bar ("Replying to Sara Malik:..."). Translate it.
onCancelReplyNexera() => voidShows the reply bar's close button and is called when it is pressed or when Escape is pressed in the field. Focus returns to the textarea.
cancelReplyLabelNexerastringAccessible name of the reply bar's close button. Translate it.
isRecordingNexerabooleanA voice message is being recorded: the row shows a red dot, the elapsed time, a waveform,
Cancel and Send instead of the field. Entering the state is announced (recordingLabel) and focus moves to Cancel
when it was in the composer; leaving it returns focus to the textarea.
recordingTimeNexerastringElapsed recording time, for example "0:07". Read as a timer, not announced every second.
recordingLabelNexerastringAnnounced when recording starts and read before the time. Translate it.
onCancelRecordingNexera() => voidCalled by the Cancel button of the recording row: discard the recording and clear isRecording.
cancelRecordingLabelNexerastringLabel of the Cancel button of the recording row. Translate it.
waveformNexeraReactNodeLive waveform of the recording. Decorative. Defaults to static bars.
textAreaRefNexeraRef<HTMLTextAreaElement>Ref to the native <textarea> (the root ref points to the composer).
classNameNexerastringExtra classes for the root, merged last so they win over the defaults.
styleNexeraCSSPropertiesInline style of the root.
labelNexeraFieldLabelContentVisible label, linked to the control by React Aria (WCAG 1.3.1, 3.3.2). Short noun, for example "Work email". Wraps instead of truncating. A placeholder is never a substitute for it. Not set: the field has no visible label. Not set: the field has no visible label of its own.
aria-labelNexerastringAccessible name when it must differ from the visible label. Prefer the visible label (WCAG 2.5.3).
Accessible name. Required when there is no visible label; translate it.
Accessible name. Optional when aria-labelledby is set.
aria-labelledbyNexerastringId(s) of element(s) that name the field; wins over the visible label.
Id(s) of element(s) that name the field; wins over aria-label.
Id(s) of visible element(s) that name the field.
validationBehaviorReact Aria"native" | "aria"Whether to use native HTML form validation to prevent form submission when the value is missing or invalid, or mark the field as required or invalid via ARIA.
enterKeyHintReact Aria"search" | "enter" | "done" | "go" | "next" | "previous" | "send"An enumerated attribute that defines what action label or icon to preset for the enter key on virtual keyboards. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/enterkeyhint).
isDisabledReact AriabooleanWhether the input is disabled.
isReadOnlyReact AriabooleanWhether the input can be selected but not changed by the user.
isRequiredReact AriabooleanWhether user input is required on the input before form submission.
isInvalidReact AriabooleanWhether the value is invalid.
validateReact Aria(value: string) => true | ValidationError | nullA function that returns an error message if a given value is invalid.
Validation errors are displayed to the user when the form is submitted
if validationBehavior="native". For realtime validation, use the isInvalid
prop instead.
autoFocusReact AriabooleanWhether the element should receive focus on render.
valueReact AriastringThe current value (controlled).
defaultValueReact AriastringThe default value (uncontrolled).
onChangeReact Aria(value: string) => voidHandler that is called when the value changes.
aria-describedbyReact AriastringIdentifies the element (or elements) that describes the object.
idReact AriastringThe element's unique identifier. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id).
autoCompleteReact AriastringDescribes the type of autocomplete functionality the input should provide if any. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefautocomplete).
maxLengthReact ArianumberThe maximum number of characters supported by the input. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefmaxlength).
minLengthReact ArianumberThe minimum number of characters required by the input. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefminlength).
autoCorrectReact AriastringAn attribute that takes as its value a space-separated string that describes what, if any, type of autocomplete functionality the input should provide. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#autocomplete).
spellCheckReact AriastringAn enumerated attribute that defines whether the element may be checked for spelling errors. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/spellcheck).
nameReact AriastringThe name of the input element, used when submitting an HTML form. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname).
formReact AriastringThe <form> element to associate the input with.
The value of this attribute must be the id of a <form> in the same document.
See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#form).
* Required. React Aria props shown are the ones most apps use; the component accepts the rest of its React Aria props too.
Accessible by default.
Built on React Aria, and covered by the WCAG 2.2 evidence generated on every build.
WCAG 2.2 evidence
10 direct · 4 supporting- 1.3.1Info and RelationshipsLevel A · tested directly
- 1.4.1Use of ColorLevel A · tested directly
- 1.4.10ReflowLevel AA · supporting test
- 1.4.11Non-text ContrastLevel AA · supporting test
- 1.4.12Text SpacingLevel AA · supporting test
- 2.1.1KeyboardLevel A · tested directly
- 2.4.3Focus OrderLevel A · tested directly
- 2.4.7Focus VisibleLevel AA · supporting test
- 3.3.1Error IdentificationLevel A · tested directly
- 3.3.2Labels or InstructionsLevel A · tested directly
- 3.3.7Redundant EntryLevel A · tested directly
- 3.3.8Accessible Authentication (Minimum)Level AA · tested directly
- 4.1.2Name, Role, ValueLevel A · tested directly
- 4.1.3Status MessagesLevel AA · tested directly
Fits any width.
Nexera components respond to the space they are given. Drag the corner of the frame, or pick a width.
Styling hooks
Pass className to add Tailwind classes (merged last). State is exposed as data attributes, so you can style it with variants like data-pressed:.
data-disableddata-hovereddata-pressed
<Composer className="data-disabled:opacity-90 shadow-sm" />Used in blocks
- ChatMessaging and collaboration
- InboxMessaging and collaboration
- Helpdesk assistantMessaging and collaboration
- Comment threadMessaging and collaboration
Related components
- ChatHeaderThe top bar of a conversation: avatar, the name as a heading, a status line and actions such as Search and More.
- ConversationItemOne conversation in a list: avatar, name, time, the last message and an unread count.
- DateSeparatorMarks where a new day starts in a conversation: a label between two lines.
- MessageBubbleOne chat message: `Direction` (incoming, outgoing) x `Type` (text, reply, file, image) x `Group` (single, continued), with sender name and avatar, time, an edited mark and read ticks.
- ReactionA reaction under a message, such as a thumbs up, that the user can add or remove.
- SystemMessageAn event in a conversation that nobody wrote, such as someone joining or a channel being renamed: a small muted pill with an optional icon.
- TypingIndicatorShows that someone is writing a reply: three dots in a bubble and a short text.