# Chatbar

A chatbar is the composer at the bottom of a chat, where people write a message, attach files and send.

A chatbar is the message composer in a chat or an assistant. It starts as one line, grows as people type, and takes files from a picker, a paste or a drop. For a plain multi-line field in a form, use a [Text Area](/docs/text-area).


## Usage

```tsx
import { Chatbar } from '@kushagradhawan/kookie-ui';

export function Composer({ onSend }: { onSend: (payload: Chatbar.ChatbarSubmitPayload) => void }) {
  return (
    <Chatbar.Root onSubmit={onSend} accept="image/*,.pdf" maxWidth="640px">
      <Chatbar.AttachmentsRow />
      <Chatbar.Textarea aria-label="Message" placeholder="Ask anything" submitOnEnter />
      <Chatbar.InlineEnd>
        <Chatbar.Send />
      </Chatbar.InlineEnd>
      <Chatbar.Row>
        <Chatbar.RowEnd>
          <Chatbar.Send />
        </Chatbar.RowEnd>
      </Chatbar.Row>
    </Chatbar.Root>
  );
}
```

## Anatomy

```tsx
<Chatbar.Root>
  <Chatbar.AttachmentsRow>
    <Chatbar.Item />
  </Chatbar.AttachmentsRow>
  <Chatbar.Textarea />
  <Chatbar.InlineStart />
  <Chatbar.InlineEnd>
    <Chatbar.Send />
  </Chatbar.InlineEnd>
  <Chatbar.Row>
    <Chatbar.RowStart>
      <Chatbar.AttachTrigger />
    </Chatbar.RowStart>
    <Chatbar.RowEnd>
      <Chatbar.Send />
    </Chatbar.RowEnd>
  </Chatbar.Row>
</Chatbar.Root>
```

- **Root:** Holds the text, the attachments and the open state, and renders the box.
- **AttachmentsRow:** A row of pills above the text, one per attachment. Hidden while empty.
- **Item:** A pill for something the message refers to that isn't a file, such as a selection or a mode.
- **Textarea:** The text input. It grows between `minLines` and `maxLines`.
- **InlineStart, InlineEnd:** Content beside the text while the chatbar is closed.
- **Row, RowStart, RowEnd:** A row of controls under the text while the chatbar is open.
- **AttachTrigger:** Opens the file picker. On its own it is a quiet icon-only Button with a paperclip.
- **Send:** Sends the message, or stops or retries a reply.

## Best practices

**Label the text.** The text has no visible label, so pass `aria-label` to `Chatbar.Textarea`, such as "Message" or "Ask anything". The placeholder can suggest what to write, but it isn't a name.

**Use Enter to send in chat.** Set `submitOnEnter` where people expect a chat app's keys: Enter sends and Shift+Enter starts a new line. Leave it off when people write long, careful messages, such as an email reply.

**Tell people which files fit.** `accept`, `maxFileSize` and `maxAttachments` reject files that don't fit, and `onAttachmentReject` gets each file and its reason. Show that reason in a message, because a file that silently fails to attach looks like a bug.

**Keep the send button in one place.** Put `Send` in `InlineEnd` for the closed chatbar and in `RowEnd` for the open one. People look for it at the end of the line.

**Put the decision above the bar.** Pass a [Notice](/docs/notice) to `notices` when the next message has to answer something, such as a permission request. Model names and the context left go in `context`, below the bar.

## Expansion

`expandOn` sets when a closed chatbar opens to `minLines` and shows its `Row`.

| expandOn | Opens when |
| --- | --- |
| `both` | The text is focused or runs past one line. This is the default |
| `focus` | The text is focused |
| `overflow` | The text runs past one line |
| `none` | Never on its own. Control it with `open` and `onOpenChange` |

With every value but `none`, an attached file opens the chatbar, and leaving it empty closes it again.

## Send button

`sendMode` on Root sets when `Send` shows: `whenDirty` (the default) shows it once there is text or a file, `always` shows it disabled while empty, and `never` hides it. `status` on `Send` lets one button follow a reply. While a reply is `submitted` or `streaming` and the box is empty, the button stops it through `onStop`. After an `error`, an empty box makes it retry through `onRetry`. With a draft in the box, it always sends.

```tsx
<Chatbar.Send status={replyStatus} onStop={stopReply} onRetry={retryReply} />
```

## Examples

### Attach button

AttachTrigger is a quiet icon-only Button with a paperclip, at the Chatbar's size. Put it at the start of the open row. Pass your own icon as its child, or render your own Button through `asChild`.

```tsx
<Chatbar.RowStart>
  <Chatbar.AttachTrigger>
    <HugeiconsIcon icon={Attachment01Icon} strokeWidth={1.75} />
  </Chatbar.AttachTrigger>
</Chatbar.RowStart>
```

### Files people paste or drop

Paste and drop are on by default. Both follow the same rules as the picker. `pasteAccept` sets other types for paste, such as images only.

```tsx
<Chatbar.Root accept=".pdf,.csv" pasteAccept="image/*" maxFileSize={10 * 1024 * 1024}>
  <Chatbar.AttachmentsRow />
  <Chatbar.Textarea aria-label="Message" />
</Chatbar.Root>
```

### Upload progress

Control `attachments` and set each one's `status` to `uploading`, `error` or `done`. An uploading pill shows a spinner, and a screen reader reads its size and status with its name.

### Things that aren't files

`Chatbar.Item` shows a pill for an object the message is about, such as a selected layer. Your app owns the list, so `onRemove` only tells you it was pressed. Leave `onRemove` out for a pill that can't be removed.

```tsx
<Chatbar.AttachmentsRow>
  <Chatbar.Item icon={<HugeiconsIcon icon={Layers01Icon} strokeWidth={1.75} />} onRemove={clearSelection}>
    Header frame
  </Chatbar.Item>
</Chatbar.AttachmentsRow>
```

## Props

### Root

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | | The text, when you control it |
| `defaultValue` | `string` | `''` | The starting text |
| `onValueChange` | `(value: string) => void` | | Called when the text changes |
| `open` | `boolean` | | Whether it is open, when you control it |
| `defaultOpen` | `boolean` | `false` | Whether it starts open |
| `onOpenChange` | `(open: boolean) => void` | | Called when it opens or closes |
| `expandOn` | `'both' \| 'focus' \| 'overflow' \| 'none'` | `'both'` | When it opens on its own |
| `minLines` | `number` | `3` | Lines shown while open |
| `maxLines` | `number` | `6` | Lines before the text scrolls |
| `sendMode` | `'whenDirty' \| 'always' \| 'never'` | `'whenDirty'` | When `Send` shows |
| `onSubmit` | `(payload: { value, attachments }) => void` | | Called on send, only when there is text or a file |
| `clearOnSubmit` | `boolean` | `true` | Clears the text and files after sending |
| `disabled` | `boolean` | | Blocks typing, sending and attaching |
| `readOnly` | `boolean` | | Keeps the text selectable but blocks editing, sending and attaching |
| `variant` | `'raised' \| 'surface' \| 'soft' \| 'outline' \| 'ghost'` | `'raised'` | The look |
| `size` | `'1' \| '2' \| '3'` | `'2'` | The size step. A theme size of 4 becomes 3 |
| `color` | `AccentColor` | Theme accent | The colour of the focus ring and send button |
| `radius` | `'none' \| 'small' \| 'medium' \| 'large' \| 'full'` | Theme radius | The corner |
| `width`, `maxWidth` | CSS width | | The width of the chatbar |
| `attachments` | `ChatbarAttachment[]` | | The files, when you control them |
| `defaultAttachments` | `ChatbarAttachment[]` | `[]` | The starting files |
| `onAttachmentsChange` | `(attachments: ChatbarAttachment[]) => void` | | Called when files are added or removed |
| `accept` | `string \| string[]` | | Accepted MIME types or extensions |
| `multiple` | `boolean` | `true` | Allows more than one file per pick, paste or drop |
| `maxAttachments` | `number` | | The most files at once |
| `maxFileSize` | `number` | | The largest file, in bytes |
| `paste` | `boolean` | `true` | Attaches files pasted into the text |
| `pasteAccept` | `string \| string[]` | `accept` | Accepted types for pasted files |
| `dropzone` | `boolean` | `true` | Attaches files dropped on the chatbar |
| `onAttachmentReject` | `(rejections: { file, reason }[]) => void` | | Called with files rejected for `type`, `size` or `count` |
| `notices` | `ReactNode` | | Messages stacked above the bar |
| `context` | `ReactNode` | | Short facts shown below the bar |
| `apiRef` | `Ref<ChatbarApi>` | | Gives `focusTextarea()` and `openFilePicker()` |

Root also accepts every attribute of the HTML `div` element. The types are on the namespace, such as `Chatbar.ChatbarAttachment`.

### Textarea

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `submitOnEnter` | `boolean` | `false` | Enter sends and Shift+Enter starts a new line |
| `asChild` | `boolean` | `false` | Renders the child element as the input |

Textarea accepts every attribute of the HTML `textarea` element except `rows`, `value` and `defaultValue`, which Root sets. `className` and `style` go on the textarea too. Your `onKeyDown` and `onPaste` run first. Call `preventDefault` in them to skip sending or attaching.

### Send

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `status` | `'ready' \| 'submitted' \| 'streaming' \| 'error'` | `'ready'` | The state of the reply |
| `onStop` | `() => void` | | Called when pressed to stop a reply |
| `onRetry` | `() => void` | | Called when pressed to retry after an error |
| `stopLabel` | `string` | `'Stop'` | The name while it stops |
| `retryLabel` | `string` | `'Retry'` | The name while it retries |
| `stopIcon`, `retryIcon` | `ReactNode` | | Icons for stop and retry |
| `clearOnSend` | `boolean` | Root's `clearOnSubmit` | Clears after sending from this button |
| `aria-label` | `string` | `'Send'` | The name while it sends |

Send also accepts Button's props except `iconOnly` and `fullWidth`. Its children replace the send icon.

### AttachmentsRow

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `renderAttachment` | `(attachment: ChatbarAttachment) => ReactNode` | | Draws your own tile for each file |
| `forceMount` | `boolean` | `false` | Renders the row while empty |

### Item

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `string` | | The label. Also names the remove button |
| `icon` | `ReactNode` | | The icon in the lead tile |
| `image` | `string` | | A thumbnail in place of the icon |
| `color` | `AccentColor` | `'gray'` | The tint of the icon tile |
| `onRemove` | `() => void` | | Shows a remove button and is called when it's pressed |
| `removeLabel` | `string` | `'Remove {label}'` | The remove button's name |

### AttachTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `accept` | `string \| string[]` | Root's `accept` | Accepted types for files picked here |
| `multiple` | `boolean` | Root's `multiple` | Allows more than one file per pick |
| `asChild` | `boolean` | `false` | Renders the child, such as a Button, as the trigger |

InlineStart, InlineEnd, Row, RowStart and RowEnd accept every attribute of the HTML `div` element, and all but RowStart and RowEnd take `asChild`.

## Accessibility

| Key | Action |
| --- | --- |
| Enter | Sends with `submitOnEnter`. Otherwise starts a new line |
| Shift+Enter | Starts a new line |
| Tab | Moves between the text, the pills and the buttons |

- **Name:** Textarea needs `aria-label`, `aria-labelledby` or a `<label htmlFor>`, and warns in development without one. Send is named "Send", "Stop" or "Retry" to match what it does. AttachTrigger is named "Add attachments" unless you pass a name.
- **Announcements:** A polite live region reads "2 files attached.", "1 file could not be attached." and "Removed report.pdf.", and "Drop files here to attach." during a drag.
- **Attachments:** The row is a list named "Attachments". Each pill reads its name, size and upload status. Its remove button is named "Remove" and the file name.
- **Focus:** Removing a pill moves focus to the next pill's remove button, or to the text when none is left. After sending, focus returns to the text. A hidden Send button leaves the tab order.
- **High contrast:** In forced colors mode the box draws a real border and shows a focus outline, since the browser drops its shadows.
- **Typing in other scripts:** Enter that confirms an input method's suggestion doesn't send.
