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.
Playground
Usage
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
<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
minLinesandmaxLines. - 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 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.
<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.
<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.
<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.
<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-labelledbyor 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.