Composer
betaPrompt input for chat and agents: auto-growing textarea, attachments, toolbar slot, send→stop button, paste/drop files and a length hint.
Default layout
Controlled, with model picker and tools
Controlled text, simulated submitted → streaming lifecycle with stop, file limits, a character limit and tool toggles.
Enter to send · Shift+Enter for a new line
Submitted, streaming, over limit and disabled
New chat home
An empty-chat start: ModelPicker and VoiceDictation in the toolbar, PromptSuggestions chips that fill and focus the input.
What are we working on?
Start typing, dictate, or pick a starting point.
Installation
npx shadcn@latest add https://kuro.abstergo.fyi/r/composer.jsonCopies the source into components/kura/composer with its dependencies (kura-theme, attachment, icon-button, kura-merge-refs, kura-motion, tooltip, use-controllable-state, use-reduced-motion, utils). Tokens come from the kura-theme item.
When to use it
Use it for
- The message box of a chat UI or agent console (“Ask anything…”).
- Any prompt field that should send on Enter, accept pasted or dropped files, and stop a streaming response.
- A prompt input that hosts a model picker, tool toggles, voice input or a slash-command menu in its toolbar.
Reach for something else when
- A single-line search or filter field — use a plain input or command-palette.
- Showing attachments outside an input (sent messages, file lists) — use attachment.
- Picking a model on its own — use model-picker.
- Voice-only input — use voice-dictation (it can also feed a composer).
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | — | Controlled text. |
| defaultValue | string | "" | Initial text when uncontrolled. |
| onValueChange | (value: string) => void | — | Called on every text change. |
| attachments | AttachmentItem[] | — | Controlled attachments ({ id, name, size?, type?, url?, status?, progress?, error?, file? }). |
| defaultAttachments | AttachmentItem[] | — | Initial attachments when uncontrolled. |
| onAttachmentsChange | (attachments: AttachmentItem[]) => void | — | Called when files are added or removed. |
| onFilesAdded | (items: AttachmentItem[]) => void | — | Called with newly accepted files (paste, drop or picker) — start uploads here and update `attachments` with status/progress. |
| onFilesRejected | (files: File[]) => void | — | Called with files rejected by accept, maxFileSize or maxFiles. |
| onSubmit | (message: ComposerMessage) => void | — | Send handler: receives { text (trimmed), attachments }. Not called while empty, over maxLength, uploading or busy. |
| onStop | () => void | — | Called by the stop button and by Escape while status is submitted or streaming. |
| status | "idle" | "submitted" | "streaming" | "error" | "idle" | Request lifecycle. submitted = waiting for the first token (stop button with a travelling arc), streaming = model writing (stop button). |
| disabled | boolean | false | Disable typing, attaching and sending (60% opacity). |
| maxLength | number | — | Character limit. The hint turns amber at 90% and red above; sending is blocked when over. |
| submitKey | "enter" | "mod+enter" | "enter" | enter = Enter sends and Shift+Enter adds a newline; mod+enter = ⌘/Ctrl+Enter sends and Enter adds a newline. |
| accept | string | false | "" | Allowed files, like <input accept> ("image/*,.pdf"). "" accepts any file; false disables attachments entirely. |
| maxFiles | number | — | Maximum number of attachments. |
| maxFileSize | number | — | Maximum size of one file in bytes. |
| clearOnSubmit | boolean | true | Clear text and attachments after a submit. |
| placeholder | string | — | Placeholder for the default layout (only used when Composer has no children). |
All native attributes of the root element, className and ref pass through.
Anatomy
- ComposerRoot and drop target. data-status, data-focused, data-dragging, data-disabled; aria-busy while submitted/streaming. With no children it renders the complete default layout.
data-slot="composer" - ComposerAttachmentsThe attachment row (renders nothing when empty). Uses attachment's AttachmentList.
data-slot="composer-attachments" - ComposerInputAuto-growing <textarea>. Enter/Shift+Enter, IME-safe, paste files, Escape stops. Accepts all textarea props.
data-slot="composer-input" - ComposerToolbarBottom row; holds ComposerTools (left) and ComposerActions (right).
data-slot="composer-toolbar" - ComposerToolsLeft group: attach button, model-picker, tool toggles.
data-slot="composer-tools" - ComposerActionsRight group: hint, voice-dictation, submit.
data-slot="composer-actions" - ComposerAttachButtonPaperclip icon button that opens the file picker.
data-slot="composer-attach" - ComposerHintCharacter count with a mini gauge (with maxLength) or a ~token estimate. data-level = ok | near | over.
data-slot="composer-hint" - ComposerSubmitSend button (ink when ready, quiet when empty) that becomes a stop button while busy; data-mode = send | stop.
data-slot="composer-submit"
Accessibility
- The textarea is labelled “Message” by default (override with aria-label or aria-labelledby) and has enterkeyhint=send.
- Over the character limit the textarea gets aria-invalid and the hint tells screen readers the limit was exceeded (announced once, not per keystroke).
- The submit button swaps its accessible name between “Send message” and “Stop generating”; the root sets aria-busy while a response is pending.
- The focus glow is on the whole composer (focus-within), so the textarea's own outline is replaced, never just removed.
- Attachments are a labelled list with “Remove <file>” buttons; removing returns focus to the textarea.
- Reduced motion: The send/stop icon swap and attachment enter/exit become plain fades (no scale or blur); the pending arc stops travelling and pulses instead.
| Keys | Action |
|---|---|
| Enter | Send (submitKey=enter). Ignored while an IME is composing. |
| Shift+Enter | Insert a newline |
| Mod+Enter | Send (both submitKey modes) |
| Escape | Stop the response while submitted/streaming |
| Mod+V | Paste text, or attach pasted files/images |
Notes for agents
- Drive `status` from your chat hook: set "submitted" when you send, "streaming" on the first token, back to "idle" when done; pass `onStop` to abort.
- With the Vercel AI SDK: status maps 1:1 from useChat's status ('ready' → 'idle').
- Compose parts in this order: ComposerAttachments, ComposerInput, ComposerToolbar > (ComposerTools, ComposerActions > ComposerHint, ComposerSubmit).
- Use `useComposer()` inside custom parts to read value, setValue, attachments, addFiles, submit, status.
- Attachment object URLs are revoked when removed, not when sent — the sent message may still display them.
- To insert text from voice-dictation or prompt-suggestions, control `value` and append to it.