Composer
A textarea and a send button wired together — growing with its content, with Enter to send and Shift+Enter for a newline.
Usage#
import { Composer } from "neelam-ui";
<Composer
placeholder="Message…"
onSubmit={(value) => send(value)}
/>onSubmit receives the trimmed, non-empty value. It fires for both routes —
Enter and the send button — so there is one path to handle.
Enter sends#
Enter submits; Shift+Enter inserts a newline. This inverts the usual
Textarea behaviour, and does so deliberately:
it is the convention every chat app shares, closely enough that it needs no
explaining to users.
Submitting is blocked when the value is empty or whitespace-only, and while
disabled — so Enter on an empty box does nothing rather than sending a blank
message.
Growing#
The field starts at one row and grows with its content up to maxHeight (200px
by default), then scrolls instead of growing forever.
There is no CSS-only way to size a textarea to its content: scrollHeight is a
measured value, not a computed one. So the height is reset to auto before each
measurement — otherwise a stale, taller height from the previous keystroke would
be what gets read back.
Clearing after send#
An uncontrolled Composer clears itself after submitting; nobody wants their
own message still sitting in the box. A controlled one does not — clearing
is left entirely to whoever owns value:
const [draft, setDraft] = useState("");
<Composer
value={draft}
onValueChange={setDraft}
onSubmit={(value) => {
send(value);
setDraft("");
}}
/>This is the same "smart default when uncontrolled, hands-off when controlled"
split Select and
Accordion make.
The ref points at the textarea
Not at the wrapping <div> — the thing you actually want after sending is the
field, to call .focus() on it again. That matches
Input's and Textarea's own ref targets.
Attachments#
Composer does not manage files itself. Render
Attachment chips with onRemove above it and
keep the staged list in your own state — which is also what lets you decide when
an upload starts.
Keyboard#
| Key | Behaviour |
|---|---|
| Enter | Sends the message. Does nothing when the field is empty or whitespace-only. |
| Shift+Enter | Inserts a newline. |
| Tab | Moves from the field to the send button, which is disabled while there is nothing to send. |
Calling preventDefault() in your own onKeyDown cancels the send, which is
the hook for a mention picker or slash-command menu that wants Enter first.
Accessibility#
- The send button always has a name via
sendLabel(default "Send message") — it renders an icon only, so without one it would announce as just "button". - The send icon is
aria-hidden. - The button is
disabledwhile the field is empty, so it is skipped byTabrather than being a focusable control that does nothing. - The wrapper shows focus with
focus-within, so the whole composer reads as focused while the caret is in the field.
The field needs a label
A placeholder is not one — it disappears as soon as the user types. Pass
aria-label="Message" (or point at a visible label with aria-labelledby),
the same rule Input documents.
API reference#
| Prop | Type | Default |
|---|---|---|
defaultValue | string | — |
maxHeightHeight (px) the field stops growing at, scrolling instead. Defaults to `200`. | number | 200 |
onSubmitCalled when the message is submitted — Enter without Shift, or clicking the send button — with the current (non-empty, trimmed) value. Named to match `onValueChange`'s own vocabulary, not the native `onSubmit` (a `<form>` submission event `TextareaHTMLAttributes` technically carries) it shadows — `Omit`ted from the base props above so this one, with its own unrelated signature, can take its place, the same reason `CommandItem`'s `onSelect` needed the identical treatment. Doesn't clear the field itself when controlled: an uncontrolled `Composer` clears on its own after submitting (nobody wants their own message sitting in the box after sending), but a controlled one leaves that entirely to whoever owns `value`, the same "smart default when uncontrolled, hands-off when controlled" split `Accordion`/`Select` already make. | ((value: string) => void) | — |
onValueChange | ((value: string) => void) | — |
sendLabelAccessible label for the send button. Defaults to `"Send message"`. | string | Send message |
value | string | — |
Also accepts every native <textarea> attribute except value, defaultValue,
rows, and onSubmit, each of which is taken over above.