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

Click to focus and expand the chatbar. Type multiple lines to see auto-resize.

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 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 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.

expandOnOpens when
bothThe text is focused or runs past one line. This is the default
focusThe text is focused
overflowThe text runs past one line
noneNever 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

PropTypeDefaultDescription
valuestringThe text, when you control it
defaultValuestring''The starting text
onValueChange(value: string) => voidCalled when the text changes
openbooleanWhether it is open, when you control it
defaultOpenbooleanfalseWhether it starts open
onOpenChange(open: boolean) => voidCalled when it opens or closes
expandOn'both' | 'focus' | 'overflow' | 'none''both'When it opens on its own
minLinesnumber3Lines shown while open
maxLinesnumber6Lines before the text scrolls
sendMode'whenDirty' | 'always' | 'never''whenDirty'When Send shows
onSubmit(payload: { value, attachments }) => voidCalled on send, only when there is text or a file
clearOnSubmitbooleantrueClears the text and files after sending
disabledbooleanBlocks typing, sending and attaching
readOnlybooleanKeeps 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
colorAccentColorTheme accentThe colour of the focus ring and send button
radius'none' | 'small' | 'medium' | 'large' | 'full'Theme radiusThe corner
width, maxWidthCSS widthThe width of the chatbar
attachmentsChatbarAttachment[]The files, when you control them
defaultAttachmentsChatbarAttachment[][]The starting files
onAttachmentsChange(attachments: ChatbarAttachment[]) => voidCalled when files are added or removed
acceptstring | string[]Accepted MIME types or extensions
multiplebooleantrueAllows more than one file per pick, paste or drop
maxAttachmentsnumberThe most files at once
maxFileSizenumberThe largest file, in bytes
pastebooleantrueAttaches files pasted into the text
pasteAcceptstring | string[]acceptAccepted types for pasted files
dropzonebooleantrueAttaches files dropped on the chatbar
onAttachmentReject(rejections: { file, reason }[]) => voidCalled with files rejected for type, size or count
noticesReactNodeMessages stacked above the bar
contextReactNodeShort facts shown below the bar
apiRefRef<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

PropTypeDefaultDescription
submitOnEnterbooleanfalseEnter sends and Shift+Enter starts a new line
asChildbooleanfalseRenders 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

PropTypeDefaultDescription
status'ready' | 'submitted' | 'streaming' | 'error''ready'The state of the reply
onStop() => voidCalled when pressed to stop a reply
onRetry() => voidCalled when pressed to retry after an error
stopLabelstring'Stop'The name while it stops
retryLabelstring'Retry'The name while it retries
stopIcon, retryIconReactNodeIcons for stop and retry
clearOnSendbooleanRoot's clearOnSubmitClears after sending from this button
aria-labelstring'Send'The name while it sends

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

AttachmentsRow

PropTypeDefaultDescription
renderAttachment(attachment: ChatbarAttachment) => ReactNodeDraws your own tile for each file
forceMountbooleanfalseRenders the row while empty

Item

PropTypeDefaultDescription
childrenstringThe label. Also names the remove button
iconReactNodeThe icon in the lead tile
imagestringA thumbnail in place of the icon
colorAccentColor'gray'The tint of the icon tile
onRemove() => voidShows a remove button and is called when it's pressed
removeLabelstring'Remove {label}'The remove button's name

AttachTrigger

PropTypeDefaultDescription
acceptstring | string[]Root's acceptAccepted types for files picked here
multiplebooleanRoot's multipleAllows more than one file per pick
asChildbooleanfalseRenders 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

KeyAction
EnterSends with submitOnEnter. Otherwise starts a new line
Shift+EnterStarts a new line
TabMoves 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.