Chat composer
Free · MITPrompt input with an auto-growing textarea, attachment chips from picker, paste or drop, a slash-command menu, model and mode selectors, a token ring against the context limit, Enter or Cmd+Enter to send and a Stop button while streaming.
Installation
$ npx shadcn@latest add https://goodui.dev/r/chat-composer.jsonThe file lands in components/goodui/chat-composer.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/chat-composer works too.
Usage
import { ChatComposer } from "@/components/goodui/chat-composer"
<ChatComposer
placeholder="Ask Aiko…"
accept="image/*,.pdf"
commands={[{ name: "summarize", description: "Summarise this chat" }, { name: "schedule", description: "Find a slot" }]}
models={MODELS}
modes={[{ id: "chat", label: "Chat" }, { id: "agent", label: "Agent" }]}
maxTokens={8000}
streaming={isStreaming}
onStop={abort}
onSubmit={(text, { attachments, model, mode }) => send(text, attachments, model, mode)}
/>When to use
- The input of any assistant chat: Enter sends, Shift+Enter adds a line, the button turns into Stop while the answer streams.
- Prompts that carry files (paste, drop or pick) and need a model or mode choice right there.
- Products with slash commands such as /summarize or /schedule.
- Plain forms and comments: use input or a textarea.
- A command palette with many actions: use command-palette-morph; the slash menu is for a handful of commands.
| If you need | Use |
|---|---|
| Only Cmd/Ctrl+Enter should send | submitOnEnter={false} |
| File attachments | accept="image/*,.pdf" (the attach button and paste/drop appear) |
| Model picker in the footer | models (ModelOption[]) with model/onModelChange |
| Chat / Research / Agent switch | modes with mode/onModeChange |
| Context limit | maxTokens (shows the ring and blocks sending above it) |
| Your tokenizer | countTokens={(text) => …} |
Examples
Controlled value filled by suggestion chips
const [draft, setDraft] = React.useState("")
<SuggestionChips suggestions={ideas} onSelect={setDraft} />
<ChatComposer value={draft} onValueChange={setDraft} onSubmit={send} />Commands that run instead of inserting
<ChatComposer commands={COMMANDS} onCommand={(c) => run(c.name)} />With a send sound
<ChatComposer onSubmit={send} sound />API reference
| Prop | Type | Default |
|---|---|---|
| autoFocus | boolean | — |
| onKeyDown Key handler on the textarea; call preventDefault to override the Enter behavior. | (e: React.KeyboardEvent<HTMLTextAreaElement>) => void | — |
| onPaste Paste handler on the textarea; pasted files become attachments when `accept` is set. | (e: React.ClipboardEvent<HTMLTextAreaElement>) => void | — |
| value | string | — |
| defaultValue | string | "" |
| onValueChange | (value: string) => void | — |
| onSubmit Called with the trimmed text and the attachments, model and mode in effect. Clears the field when uncontrolled. | (text: string, meta: { attachments: ComposerAttachment[]; model?: string; mode?: string }) => void | — |
| streaming The answer is streaming: the send button becomes Stop and calls onStop. | boolean | false |
| onStop | () => void | — |
| attachments | ComposerAttachment[] | — |
| defaultAttachments | ComposerAttachment[] | [] |
| onAttachmentsChange | (attachments: ComposerAttachment[]) => void | — |
| accept Accept attribute for the file picker, e.g. "image/*,.pdf". Omit to hide the attach button. | string | — |
| maxAttachments | number | 10 |
| commands Typing "/" at the start opens this list. | SlashCommand[] | — |
| onCommand Runs when a command is picked; by default the command is inserted as "/name ". | (command: SlashCommand) => void | — |
| models Models for a built-in ModelPicker in the footer. | ModelOption[] | — |
| model | string | — |
| defaultModel | string | — |
| onModelChange | (id: string) => void | — |
| modes Modes shown as a segmented control, e.g. Chat / Research / Code. | ComposerMode[] | — |
| mode | string | — |
| defaultMode | string | — |
| onModeChange | (id: string) => void | — |
| maxTokens Context limit; shows the counter and blocks sending above it. | number | — |
| countTokens Your tokenizer; defaults to about one token per four characters. | (text: string) => number | — |
| placeholder Textarea placeholder; also its accessible name. | string | — |
| minRows | number | 1 |
| maxRows | number | 8 |
| submitOnEnter Enter sends (Shift+Enter adds a line). false: only Cmd/Ctrl+Enter sends. | boolean | true |
| disabled | boolean | — |
| footer Extra controls at the start of the footer. | React.ReactNode | — |
| labels Text for built-in controls and screen readers; pass translations here. | Partial<ChatComposerLabels> | — |
| sound Plays "send" on submit when the visitor has sounds on (lib/goodui-sounds); a sound name picks another. Off by default. | boolean | SoundName | — |
Also accepts every <form> attribute.
Accessibility
| Enter | Sends (unless submitOnEnter is false or an IME composition is in progress). Shift+Enter inserts a new line. |
| Cmd/Ctrl+Enter | Always sends. |
| / at the start | Opens the command menu, filtered by what follows. |
| ArrowUp / ArrowDown | Moves through the command menu. |
| Enter / Tab | Picks the active command; Escape closes the menu. |
- The textarea has an aria-label (the placeholder) and is described by a hidden hint explaining Enter and Shift+Enter.
- The command menu is a listbox with option rows; the modes are a radiogroup; the send button's label switches between Send and Stop.
- The token counter is read as "1.2K of 8K tokens" or "Over the token limit"; attachment remove buttons are labelled with the file name.
- Built-in strings go through the labels prop for translation.
Motion
- The textarea grows with its content between minRows and maxRows (height set from scrollHeight, no transition).
- The command menu springs up from the bottom edge (stiffness 520, damping 34) with rows staggered 30 ms; the active row highlight glides on a shared-layout spring.
- Attachment chips spring in at 0.8 scale and the row's height animates to fit; the send arrow and stop square swap with a rotate-and-scale spring; the token ring eases over 0.35 s.
- Reduced motion: menus and chips fade, the icon swap is a fade, the ring jumps.
Notes for AI
Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.
- onSubmit gets the trimmed text and { attachments, model, mode }. When uncontrolled, the field and attachments clear after submit.
- While streaming is true the button calls onStop instead of submitting; sending is also blocked when the text is empty (and no attachments) or over maxTokens.
- The default token estimate is characters / 4; pass countTokens for your tokenizer.
- Attachments created from files get an object URL for image previews; revoke them yourself if you keep attachments around for long.
- The built-in model picker is model-picker (installed alongside); pass models to show it.
FAQ
How do I add my own buttons to the footer?
Pass them as `footer`; they render after the attach button.
Can I keep sending on Enter but insert newlines on mobile?
Set submitOnEnter={false} on touch devices; the send button and Cmd/Ctrl+Enter still work.
Related
Model dropdown grouped by provider with icon slots, speed and quality meters that grow in, context window and price per million tokens, a gliding highlight and full keyboard support with type-ahead.
User, assistant and system messages with avatars, grouping, a hover actions row (copy, good/bad, retry, edit), markdown-ish rendering of lists, quotes and fenced code into CodeBlocks, and a blinking caret while the answer streams.
Starter prompts that stagger in on springs, lift on hover and fill the composer on click, as pills, cards with hints or a scrolling row with faded edges; swapping the set replays the entrance.
Reveals an answer token by token at a steady speed with a cursor that blinks while waiting for more, stop and resume, word or character mode, and smooth catch-up when uneven SSE chunks arrive; reduced motion shows sentence chunks.
Context window usage bar whose segments (system, history, tools, response) spring to new widths, with rolling numbers, a legend, and amber then rose tones near and over the limit.