Chatbar

Expandable chat input with auto-resize, file attachments, and drag-and-drop uploads.

This component's API and documentation are under review and may change. Use with caution in production.

Use Chatbar to create rich messaging experiences with expandable text areas, file attachments, and intuitive submission flows. Designed for AI chat interfaces, customer support, and collaborative applications.

Playground

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

Installation

npm install @kushagradhawan/kookie-ui

Usage

import { Chatbar } from '@kushagradhawan/kookie-ui';
 
export function MyChat() {
  return (
    <Chatbar.Root onSubmit={({ value }) => sendMessage(value)}>
      <Chatbar.Textarea aria-label="Message" placeholder="Type a message..." />
      <Chatbar.InlineEnd>
        <Chatbar.Send />
      </Chatbar.InlineEnd>
    </Chatbar.Root>
  );
}

Anatomy

Chatbar is a compound component with the following parts:

<Chatbar.Root>
  <Chatbar.AttachmentsRow />           {/* Horizontal scrollable attachment list */}
  <Chatbar.Textarea />                 {/* Auto-resizing text input */}
  <Chatbar.InlineStart />              {/* Left slot when compact (closed) */}
  <Chatbar.InlineEnd />                {/* Right slot when compact (closed) */}
  <Chatbar.Row>                        {/* Row visible when expanded (open) */}
    <Chatbar.RowStart />               {/* Left slot in expanded row */}
    <Chatbar.RowEnd />                 {/* Right slot in expanded row */}
  </Chatbar.Row>
  <Chatbar.AttachTrigger />            {/* File picker trigger button */}
  <Chatbar.Send />                     {/* Submit button */}
</Chatbar.Root>

Props

Root

PropTypeDefaultDescription
valuestring-Controlled text input value
defaultValuestring""Initial value for uncontrolled usage
onValueChange(value: string) => void-Callback when text value changes
openboolean-Controlled expansion state
defaultOpenbooleanfalseInitial expansion state for uncontrolled usage
onOpenChange(open: boolean) => void-Callback when expansion state changes
expandOn'none' | 'focus' | 'overflow' | 'both''both'When to auto-expand: on focus, when content overflows, or both. 'none' never auto-opens or auto-collapses
minLinesnumber3Minimum visible lines when expanded. The compact state is always one line
maxLinesnumber6Maximum visible lines before scrolling
sendMode'always' | 'whenDirty' | 'never''whenDirty'Send button visibility only: always visible, visible with content, or not rendered
disabledboolean-Blocks text editing, submitting and every way of adding attachments
readOnlyboolean-Allows selection; blocks editing, submitting and adding attachments
onSubmit(payload: Chatbar.ChatbarSubmitPayload) => void-Called with text and attachments. Only fires when there is text or at least one attachment
size'1' | '2' | '3''2'Component density: 1 for compact, 2 for standard, 3 for prominent
variant'surface' | 'outline' | 'classic' | 'ghost' | 'soft''surface'Visual style variant
colorAccentColor-Accent color for focus states and send button
radius'none' | 'small' | 'medium' | 'large' | 'full'-Corner radius override
material'solid' | 'translucent'-Background appearance for layered interfaces
widthCSSProperties['width']-Fixed width
maxWidthCSSProperties['maxWidth']-Maximum width constraint
apiRefRef<Chatbar.ChatbarApi>-Imperative API for programmatic control

Attachment Props

PropTypeDefaultDescription
attachmentsChatbarAttachment[]-Controlled attachments array
defaultAttachmentsChatbarAttachment[][]Initial attachments for uncontrolled usage
onAttachmentsChange(attachments: ChatbarAttachment[]) => void-Callback when attachments change
acceptstring | string[]-Accepted MIME types or extensions (e.g., 'image/*', '.pdf')
multiplebooleantrueAllow more than one file per pick, paste or drop. Extra files are rejected with reason count
maxAttachmentsnumber-Maximum number of attachments
maxFileSizenumber-Maximum file size in bytes
pastebooleantrueEnable paste-to-attach for clipboard files
pasteAcceptstring | string[]-Accepted types for pasted files. Replaces accept for paste, so it can be wider or narrower
clearOnSubmitbooleantrueClear message and attachments after submitting with Enter or Send. Clearing calls onValueChange(''), also for a controlled value
onAttachmentReject(rejections: Chatbar.ChatbarAttachmentRejection[]) => void-Called with files rejected for type, size or count
dropzonebooleantrueEnable drag-and-drop file uploads

Textarea

PropTypeDefaultDescription
submitOnEnterbooleanfalsePlain Enter submits and never inserts a newline. Shift+Enter inserts a newline. Works with every sendMode
aria-labelstring-Accessible name (required for accessibility)
onKeyDownKeyboardEventHandler-Runs before the built-in handler. Call preventDefault to skip submit-on-Enter
onPasteClipboardEventHandler-Runs before the built-in handler. Call preventDefault to skip paste-to-attach
asChildboolean-Merge props onto child element

Textarea does not accept rows. Its height is driven by minLines and maxLines on Root.

Send

PropTypeDefaultDescription
clearOnSendbooleanRoot's clearOnSubmitClear message and attachments after sending. Overrides Root clearOnSubmit for this button
aria-labelstring"Send"Accessible button name

AttachmentsRow

PropTypeDefaultDescription
forceMountboolean-Render even when empty
renderAttachment(attachment: ChatbarAttachment) => ReactNode-Custom attachment tile renderer

AttachTrigger

PropTypeDefaultDescription
acceptstring | string[]Root's acceptOverrides Root accept for files picked through this trigger
multiplebooleanRoot's multipleOverride multiple selection
asChildboolean-Merge props onto child element

AttachTrigger is disabled while Root is disabled or readOnly.

Types

Types are exported on the Chatbar namespace. Use them as Chatbar.X:

import { Chatbar } from '@kushagradhawan/kookie-ui';
 
const handleSubmit = (payload: Chatbar.ChatbarSubmitPayload) => {};

Available types: ChatbarApi, ChatbarAttachment, ChatbarAttachmentStatus, ChatbarAttachmentRejection, ChatbarRejectReason, ChatbarSubmitPayload, ChatbarSendMode, ChatbarExpandOn, and the part props types (ChatbarRootProps, ChatbarTextareaProps, ChatbarInlineSlotProps, ChatbarAttachmentsRowProps, ChatbarAttachmentProps, ChatbarAttachTriggerProps, ChatbarRowProps, ChatbarRowSlotProps, ChatbarSendProps).

ChatbarAttachment

interface ChatbarAttachment {
  id: string;
  name: string;
  size: number;
  type: string;
  file: File;
  url?: string;
  status?: 'idle' | 'uploading' | 'error' | 'done';
  progress?: number;
  meta?: Record<string, unknown>;
}

For images, url is an object URL. Chatbar revokes it when the attachment is removed. When a submit clears the chatbar, you own the URLs in the payload and should revoke them with URL.revokeObjectURL when done. If the chatbar is not cleared, Chatbar keeps ownership.

ChatbarSubmitPayload

interface ChatbarSubmitPayload {
  value: string;
  attachments: ChatbarAttachment[];
}

ChatbarAttachmentRejection

type ChatbarRejectReason = 'type' | 'size' | 'count';
 
interface ChatbarAttachmentRejection {
  file: File;
  reason: ChatbarRejectReason;
}

ChatbarApi

interface ChatbarApi {
  focusTextarea: () => void;
  // Works without an AttachTrigger and with dropzone={false}.
  // Does nothing when disabled or readOnly.
  openFilePicker: () => void;
}

Variants

Use the variant prop to set chatbar style.

Classic

Elevated with shadow for maximum visual impact and prominent chat interfaces.

<Chatbar.Root variant="classic">
  <Chatbar.Textarea aria-label="Message" />
  <Chatbar.InlineEnd>
    <Chatbar.Send />
  </Chatbar.InlineEnd>
</Chatbar.Root>

Surface

Subtle border with background fill. The default variant for most contexts.

<Chatbar.Root variant="surface">
  <Chatbar.Textarea aria-label="Message" />
  <Chatbar.InlineEnd>
    <Chatbar.Send />
  </Chatbar.InlineEnd>
</Chatbar.Root>

Soft

Colored background without border for accent-colored interfaces.

<Chatbar.Root variant="soft" color="blue">
  <Chatbar.Textarea aria-label="Message" />
  <Chatbar.InlineEnd>
    <Chatbar.Send />
  </Chatbar.InlineEnd>
</Chatbar.Root>

Outline

Border only, no background fill for minimal visual weight.

<Chatbar.Root variant="outline">
  <Chatbar.Textarea aria-label="Message" />
  <Chatbar.InlineEnd>
    <Chatbar.Send />
  </Chatbar.InlineEnd>
</Chatbar.Root>

Ghost

Minimal styling with background only on hover/focus for utility contexts.

<Chatbar.Root variant="ghost">
  <Chatbar.Textarea aria-label="Message" />
  <Chatbar.InlineEnd>
    <Chatbar.Send />
  </Chatbar.InlineEnd>
</Chatbar.Root>

Sizes

Set size for component density: 1 (compact), 2 (standard), 3 (prominent).

Size 1

For compact interfaces like inline replies or comment sections.

<Chatbar.Root size="1">
  <Chatbar.Textarea aria-label="Reply" />
</Chatbar.Root>

Size 2

For standard chat interfaces. The default size.

<Chatbar.Root size="2">
  <Chatbar.Textarea aria-label="Message" />
</Chatbar.Root>

Size 3

For prominent AI chat experiences and hero sections.

<Chatbar.Root size="3">
  <Chatbar.Textarea aria-label="Ask anything" />
</Chatbar.Root>

Colors

Use the color prop (any accent color) to set the accent color for focus states and the send button. With color="gray" the send button fills with step 12 on its own.

<Chatbar.Root color="blue">
  <Chatbar.Textarea aria-label="Message" />
  <Chatbar.InlineEnd>
    <Chatbar.Send />
  </Chatbar.InlineEnd>
</Chatbar.Root>

Material

Use the material prop for translucent surfaces over complex backgrounds.

Theme

Chatbar automatically inherits the theme's material setting:

<Theme material="translucent">
  <Chatbar.Root variant="classic">
    <Chatbar.Textarea aria-label="Message" />
  </Chatbar.Root>
</Theme>

Custom

Override the theme's material for specific effects:

<Theme material="solid">
  <Chatbar.Root material="translucent" variant="classic">
    <Chatbar.Textarea aria-label="Message" />
  </Chatbar.Root>
</Theme>

States

Disabled

Blocks text editing, submitting, and every attach path: trigger, paste, drop and apiRef. AttachTrigger is disabled and attachment remove buttons are hidden.

<Chatbar.Root disabled>
  <Chatbar.Textarea aria-label="Message" placeholder="Chat is disabled" />
</Chatbar.Root>

Read Only

Allows viewing and selection. Blocks editing, submitting and adding attachments, the same as disabled. Remove buttons are hidden.

<Chatbar.Root readOnly defaultValue="This message cannot be edited">
  <Chatbar.Textarea aria-label="Message" />
</Chatbar.Root>

Expansion

The expandOn prop controls when the chatbar expands from compact to full height.

Focus

Expand when the textarea receives focus:

<Chatbar.Root expandOn="focus">
  <Chatbar.Textarea aria-label="Message" />
</Chatbar.Root>

Overflow

Expand when content exceeds one line:

<Chatbar.Root expandOn="overflow">
  <Chatbar.Textarea aria-label="Message" />
</Chatbar.Root>

Both

Expand on either focus or overflow. The default behavior:

<Chatbar.Root expandOn="both">
  <Chatbar.Textarea aria-label="Message" />
</Chatbar.Root>

None

Never auto-open, even when a file is attached, and never auto-collapse on blur. Control it with the open prop:

const [open, setOpen] = useState(false);
 
<Button onClick={() => setOpen(!open)}>Toggle</Button>
 
<Chatbar.Root expandOn="none" open={open} onOpenChange={setOpen}>
  <Chatbar.Textarea aria-label="Message" />
</Chatbar.Root>

Attachments

File Picker

Use AttachTrigger with asChild to create a custom file picker button. Its accept overrides Root accept for files picked through it:

<Chatbar.Root accept="image/*,.pdf" maxFileSize={5 * 1024 * 1024}>
  <Chatbar.AttachmentsRow />
  <Chatbar.Textarea aria-label="Message" />
  <Chatbar.Row>
    <Chatbar.RowStart>
      <Chatbar.AttachTrigger asChild>
        <IconButton
          variant="ghost"
          size="2"
          color="gray"
          aria-label="Attach file"
        >
          <HugeiconsIcon icon={Attachment01Icon} strokeWidth={1.75} />
        </IconButton>
      </Chatbar.AttachTrigger>
    </Chatbar.RowStart>
  </Chatbar.Row>
</Chatbar.Root>

Drag and Drop

Dropzone is enabled by default. Files can be dropped anywhere on the chatbar with visual feedback. The same accept, multiple, maxFileSize and maxAttachments rules apply to pick, paste and drop:

<Chatbar.Root
  dropzone={true}
  accept="image/*"
  maxAttachments={5}
  onAttachmentReject={(rejections) => {
    rejections.forEach(({ file, reason }) => {
      toast.error(`${file.name} rejected: ${reason}`);
    });
  }}
>
  <Chatbar.AttachmentsRow />
  <Chatbar.Textarea aria-label="Message" />
</Chatbar.Root>

Paste to Attach

Paste images or files directly from the clipboard. A pasted file is attached once. pasteAccept replaces accept for paste. If every pasted file is rejected, the normal text paste still happens.

<Chatbar.Root paste={true} pasteAccept="image/*">
  <Chatbar.AttachmentsRow />
  <Chatbar.Textarea aria-label="Message" />
</Chatbar.Root>

Custom Rendering

Override the default attachment tile renderer:

<Chatbar.Root>
  <Chatbar.AttachmentsRow
    renderAttachment={(attachment) => (
      <Card size="1" variant="surface">
        <Flex align="center" gap="2" p="2">
          <Text size="1">{attachment.name}</Text>
          <Text size="1" emphasis="medium">{Math.ceil(attachment.size / 1024)} KB</Text>
        </Flex>
      </Card>
    )}
  />
  <Chatbar.Textarea aria-label="Message" />
</Chatbar.Root>

Send Mode

sendMode controls only whether the Send button is visible. Submitting, with Enter or Send, always requires text or at least one attachment. A hidden Send is aria-hidden and not focusable.

When Dirty

Show Send only when there is text or an attachment. The default:

<Chatbar.Root sendMode="whenDirty">
  <Chatbar.Textarea aria-label="Message" />
  <Chatbar.InlineEnd>
    <Chatbar.Send />
  </Chatbar.InlineEnd>
</Chatbar.Root>

Always

Always show Send. With nothing to send it is visible but disabled:

<Chatbar.Root sendMode="always">
  <Chatbar.Textarea aria-label="Message" />
  <Chatbar.InlineEnd>
    <Chatbar.Send />
  </Chatbar.InlineEnd>
</Chatbar.Root>

Never

Never render Send. Pair with submitOnEnter so Enter submits:

<Chatbar.Root sendMode="never">
  <Chatbar.Textarea aria-label="Message" submitOnEnter />
</Chatbar.Root>

Imperative API

Use apiRef for programmatic control. openFilePicker() works without an AttachTrigger and with dropzone={false}, and does nothing when the chatbar is disabled or read-only.

import { useRef } from 'react';
import { Button, Chatbar, Flex } from '@kushagradhawan/kookie-ui';
 
export function ChatWithShortcuts() {
  const chatbarApi = useRef<Chatbar.ChatbarApi>(null);
 
  return (
    <Flex direction="column" gap="2">
      <Flex gap="2">
        <Button
          variant="soft"
          color="gray"
          onClick={() => chatbarApi.current?.focusTextarea()}
        >
          Focus chat
        </Button>
        <Button
          variant="soft"
          color="gray"
          onClick={() => chatbarApi.current?.openFilePicker()}
        >
          Attach files
        </Button>
      </Flex>
      <Chatbar.Root apiRef={chatbarApi}>
        <Chatbar.AttachmentsRow />
        <Chatbar.Textarea aria-label="Message" />
      </Chatbar.Root>
    </Flex>
  );
}

Data Attributes

The Root component exposes data attributes for custom styling:

AttributeValuesDescription
data-state'open' | 'closed'Expansion state
data-disabledPresent when disabledDisabled state
data-readonlyPresent when read-onlyRead-only state
data-drop-activePresent during dragDrag-and-drop active
data-accent-colorColor nameCurrent accent color

Accessibility

Chatbar provides accessibility through semantic markup, keyboard support and live announcements.

Keyboard Navigation

  • Enter - Submit when submitOnEnter is on. A plain Enter never inserts a newline, even with nothing to send
  • Shift+Enter - Insert a new line
  • Tab - Move between interactive elements. A hidden Send is skipped
  • Enter used to confirm IME composition is ignored

ARIA Attributes

  • aria-label or aria-labelledby required on Textarea for an accessible name
  • role="list" on the attachments container and role="listitem" on each attachment
  • Remove buttons are labelled "Remove" plus the file name
  • A hidden Send carries aria-hidden

Screen Readers

  • A polite live region announces attachments added, rejected and removed
  • The live region announces a hint while files are dragged over the chatbar
  • Image tiles carry visually hidden name, size and status

Focus Management

  • Clicking a non-interactive area of the chatbar focuses the textarea
  • After removing an attachment, focus moves to the neighbouring remove button, or back to the textarea
  • Blur collapse is held off while the file dialog is open

Motion

  • Layout animations respect prefers-reduced-motion

Changelog

Added

  • Compound component architecture with Root, Textarea, Send, AttachmentsRow, AttachTrigger, Row, RowStart, RowEnd, InlineStart, InlineEnd
  • Controlled and uncontrolled value, open, and attachments states
  • File picker, drag-and-drop, and paste-to-attach support
  • Auto-resizing textarea with configurable line limits
  • Imperative API via apiRef for programmatic control
  • Multiple visual variants: surface, outline, classic, ghost, soft
  • Material support for translucent surfaces
  • Comprehensive accessibility with ARIA attributes and keyboard navigation
  • Exported types on the Chatbar namespace: ChatbarApi, ChatbarAttachment, ChatbarAttachmentRejection, ChatbarRejectReason, ChatbarSubmitPayload, ChatbarSendMode, ChatbarExpandOn, and part props types
  • Polite live region for attachment and drag-over announcements

Changed

  • Root no longer accepts asChild
  • Root no longer sets aria-expanded
  • sendMode controls Send visibility only
  • Send clearOnSend defaults to Root clearOnSubmit
  • pasteAccept replaces accept for paste
  • color is typed as an accent color and radius as 'none' | 'small' | 'medium' | 'large' | 'full'

Removed

  • highContrast on Send. Gray fills with step 12 on its own.

Fixed

  • sendMode="never" with submitOnEnter now submits
  • Empty messages with no attachments no longer submit
  • Clearing now calls onValueChange('') for a controlled value
  • Plain Enter with submitOnEnter no longer inserts a newline; IME composition Enter is ignored
  • disabled and readOnly block every attach path and hide remove buttons
  • expandOn="none" no longer opens on attach or collapses on blur
  • multiple={false} applies to paste and drop
  • Pasted files attach once, and text paste still happens when all pasted files are rejected
  • apiRef.openFilePicker() works without an AttachTrigger and with dropzone={false}
  • Image preview URLs are revoked on removal

© 2026 Kushagra Dhawan. Licensed under MIT. GitHub.

Theme

Accent color

Gray color

Appearance

Radius

Scaling

Panel background