Skip to content

Documentation

neelam-ui

Search documentation

Getting Started
Forms
Overlays
Navigation
Data Display
Layout
AI & Chat
Blocks
GitHub repository

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#

Keyboard shortcuts
KeyBehaviour
EnterSends the message. Does nothing when the field is empty or whitespace-only.
Shift+EnterInserts a newline.
TabMoves 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 disabled while the field is empty, so it is skipped by Tab rather 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#

Props for Composer
PropTypeDefault
defaultValuestring
maxHeight

Height (px) the field stops growing at, scrolling instead. Defaults to `200`.

number200
onSubmit

Called 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)
sendLabel

Accessible label for the send button. Defaults to `"Send message"`.

stringSend message
valuestring

Also accepts every native <textarea> attribute except value, defaultValue, rows, and onSubmit, each of which is taken over above.