Prompt input
The composer: a growing textarea with slots for context, attachments and tools, IME-safe Enter to send, Escape to stop, and paste and drop that turn into files.
On this page
Installation
Usage
import {
PromptInput,
PromptInputActions,
PromptInputAttachButton,
PromptInputSubmit,
PromptInputTextarea,
PromptInputToolbar,
PromptInputTools,
} from "@/components/ui/chatcn/prompt-input"
const { sendMessage, status, stop } = useChat()
<PromptInput
onSubmit={({ text }) => sendMessage({ text })}
status={status === "streaming" ? "streaming" : status === "submitted" ? "submitting" : "idle"}
onStop={stop}
onFiles={(files) => upload(files)}
>
<PromptInputTextarea placeholder="Ask anything…" />
<PromptInputToolbar>
<PromptInputTools>
<PromptInputAttachButton />
</PromptInputTools>
<PromptInputActions>
<PromptInputSubmit />
</PromptInputActions>
</PromptInputToolbar>
</PromptInput>It is a form. Uncontrolled, it clears itself after sending; controlled with value and onValueChange, you clear it. Put attachments in PromptInputAttachments and context chips in PromptInputHeader; both render nothing while empty. usePromptInput() reads the state for custom tools.
Examples
Paste and drop
A pasted screenshot becomes a file instead of a lost paste. Text over pasteTextAsFileAt characters becomes Pasted text.txt. Ordinary text pastes are never blocked. Files outside accept are refused before the drop, and onFilesRejected gets the reason.
States
Floating
The one place translucency is allowed: chrome floating over content. It falls back to a solid surface where backdrop-filter is unsupported.
Minimal
Hero
Props
| Prop | Type | Notes |
|---|---|---|
value, defaultValue, onValueChange | string | Controlled or uncontrolled text |
onSubmit | ({ text }) => void | Not called while empty, disabled or busy |
status | "idle" | "submitting" | "streaming" | Streaming shows Stop and enables Escape |
onStop | () => void | Stop button and Escape |
disabled | boolean | Read-only, rate limited or offline. Say why in the placeholder |
variant | "default" | "floating" | "minimal" | "hero" | |
onFiles | (files, source) => void | source is paste, drop or picker. Enables drop and the attach button |
onFilesRejected | (rejections) => void | Each with reason: type, size or count |
accept, maxFileSize, maxFiles | Same syntax as <input accept>; bytes | |
pasteTextAsFileAt | number | Characters. Off by default |
hasContent | boolean | Allow sending with empty text, for example with files attached |
submitOn | "enter" | "mod-enter" | "auto" | auto sends on Enter with a mouse, on Cmd/Ctrl+Enter on touch |
clearOnSubmit | boolean | Uncontrolled only. Default true |
dropLabel, dropHint | ReactNode | Drop overlay copy |
| Part | Notes |
|---|---|
PromptInputTextarea | Grows to 40% of the viewport, then scrolls. 16 px on touch screens so iOS never zooms |
PromptInputHeader | Slot above the text for ContextChips |
PromptInputAttachments | Slot above the text for an AttachmentList |
PromptInputToolbar | The row under the text |
PromptInputTools, PromptInputActions | Leading group that shrinks, trailing group that never does |
PromptInputButton | Ghost toolbar button with tooltip and shortcut |
PromptInputAttachButton | Opens the file picker. Hidden without onFiles |
PromptInputSubmit | SendButton wired to the composer |
Keyboard
| Key | Action |
|---|---|
Enter | Send. Never while an IME is composing |
Shift+Enter | New line |
Cmd+Enter, Ctrl+Enter | Send, in every submitOn mode |
Esc | Stop the response while streaming, from anywhere in the composer |
| Click on the surface | Puts the caret in the textarea |
Accessibility
- The textarea is labelled Message by default. The send button's name follows its state: Send message, Sending, Stop generating.
- When streaming starts, a polite live region says Generating, press Escape to stop. Once, not per token.
- Composition events and
keyCode 229are both checked, so Japanese, Chinese and Korean input never sends early, Safari included. - Dragging is never the only way in: the attach button opens the same flow. Rejected files come back with a reason you can show.
- Focus uses the input focus ring from the spec (1 px ink plus a soft halo) and never animates. In forced colors the composer keeps a border.
Motion
- Growth is instant and keeps the caret where it was: an animated height would lag behind typing.
- The drop overlay fades in over 150 ms with its content settling from 0.98. Rejection swaps the icon with a 150 ms blur crossfade.
- Send to Stop is the send button's morph, in the same footprint.