# ChatComposer

**When to use:** The field a chat message is written in, with its send button: under a [ChatMessageList](ChatMessageList.md), which shows a whole chat put together. For multiline text in a form, with a label and validation, use [Textarea](Textarea.md).

**Keywords:** message input, chat input, prompt, compose, reply box, enter to send, submit, attachment

**Import:** `import { ChatComposer } from '@ahrowe/ui'`  
**Types:** `import type { ChatComposerProps, ChatComposerLabels } from '@ahrowe/ui'`

```tsx
import { ActionIcon, ChatComposer } from '@ahrowe/ui';
import { faFaceSmile, faPaperclip } from '@fortawesome/free-solid-svg-icons';

// Uncontrolled: it clears itself after sending
<ChatComposer onSend={(text) => send(text)} />

// AI chat: a stop button replaces send while the answer streams
<ChatComposer
  onSend={ask}
  onStop={isStreaming ? stop : undefined}
  labels={{ placeholder: 'Ask anything' }}
/>

// Buttons of your own around the field
<ChatComposer
  onSend={send}
  startContent={<ActionIcon icon={faPaperclip} aria-label='Attach a file' onClick={openFilePicker} />}
  endContent={<ActionIcon icon={faFaceSmile} aria-label='Emoji' onClick={openEmojiPicker} />}
/>

// Controlled, e.g. to keep a draft per conversation: sending clears it through onChange('')
<ChatComposer value={draft} onChange={setDraft} onSend={send} />

// Native attributes go to the textarea; an Enter your onKeyDown prevents is not sent
<ChatComposer
  onSend={send}
  maxLength={2000}
  autoFocus
  onKeyDown={(event) => {
    if (event.key === 'Enter' && isMentionListOpen) {
      event.preventDefault();
      pickMention();
    }
  }}
/>
```

**Keys:**

| Key | Does |
|-----|------|
| `Enter` | Sends. On a touch screen it starts a new line instead, since that keyboard has no other way to, and only the button sends |
| `Shift+Enter` | A new line |
| `Ctrl+Enter` / `⌘+Enter` | Sends, on a touch screen too |

The `Enter` that confirms a word in a Japanese, Chinese or Korean input method never sends,
including in Safari, which reports it as an ordinary `Enter`.

`onSend` gets the text trimmed, and is not called while the field is empty or only whitespace;
the send button is disabled then too. The focus stays in the field after sending, so a phone
keyboard stays open. The field grows with the text up to seven lines, then scrolls, and measures
again when its width changes.

**Buttons of your own.** `startContent` and `endContent` sit beside the field while the text fits
in it. Once it needs more lines than `rows` (a second one, by default), the field takes the whole
width and the buttons move to a row below it: `startContent` at the start, `endContent` and the
send button at the end, the usual toolbar of an AI chat. So several of them never leave the field
too narrow to write in. They move back once the field is empty, usually after sending, rather
than as soon as the wider field fits the text on one line, which would wrap it again. With only
the send button there is nothing to move, and it stays beside the field.

While `onStop` is set the send button is a stop button that calls it, and `Enter` sends nothing,
so the user can type the next message while an answer streams in.

**Key props:**

| Prop | Type | Description |
|------|------|-------------|
| `onSend` | `(text: string) => void` | Called with the trimmed text (required) |
| `value` | `string` | Controlled value. Sending clears it through `onChange('')` |
| `defaultValue` | `string` | Initial value, uncontrolled |
| `onChange` | `(value: string) => void` | Every change |
| `onStop` | `() => void` | Swaps send for a stop button while set |
| `onKeyDown` | `KeyboardEventHandler` | Runs before the composer's own keys; `preventDefault()` on an Enter keeps it from sending |
| `disabled` | `boolean` | Disables the field and both buttons |
| `rows` | `number` | The empty field's height in lines, default 1. It grows from there to seven |
| `startContent` | `ReactNode` | Before the field: attach, emoji. Below it once the text wraps, see above |
| `endContent` | `ReactNode` | Between the field and the send button. Below it once the text wraps |
| `ref` | `Ref<HTMLTextAreaElement>` | Ref to the inner `<textarea>`, e.g. to focus it when a conversation opens |

Every other native attribute (`maxLength`, `autoFocus`, `name`, `spellCheck`, `onPaste`, `aria-*`,
`data-*`) goes to the `<textarea>`, as on [Input](Input.md); `className` and `style` go to the
root. An `aria-label` replaces the placeholder as the field's name, and `readOnly` also keeps it
from sending.

**Global defaults:** adopts `ConfigProvider`. See [ConfigProvider.md](ConfigProvider.md).

**Slots:** `input` (the `<textarea>`) `button` (send or stop)

**Labels:** `placeholder` (also the field's accessible name) `send` `stop`: the text this component renders of its own. Pass `labels` to override any of them, on the component or app-wide through `ConfigProvider`; they merge per key. Type: `ChatComposerLabels`.
