Prompt Input
A chat composer that starts as one calm line, grows into a card as you write, and moves down once the conversation starts. Enter sends, safe with Japanese and Chinese input. Paste, drop or pick files with previews, progress and retry. @ adds files and / runs commands from a menu at the caret. A model picker with number keys, an effort slider that comes alive at Max, a context ring, dictation with a live waveform, tool chips, a queue for messages typed while a reply streams, and drafts that survive a reload.
The composer is the part of a chat people touch most, so every keystroke has to feel right. Prompt Input starts as one calm line with the + on the left and send on the right. As soon as the text wraps or files are added, it grows into a card with the tools underneath, and it settles back when cleared.
Everything is composed from parts. Use only the textarea and send button, or add files, menus, a model picker, effort, context usage, dictation, tool chips and a queue. The parts share one context, so they work together without wiring.
- Grows with you. Auto height with field-sizing, a compact single line that becomes a card when text wraps or tools crowd it, and a smooth height change instead of a jump.
- Enter that respects typing. Enter sends and Shift+Enter adds a line. Composition with Japanese, Chinese or Korean input is never interrupted. On touch screens Enter adds a line, because the keyboard has no Shift.
- Never drops a key. The textarea stays uncontrolled by animations and menus, inserts use the browser’s own undo stack, and typing anywhere on the page can jump into the field with focusOnType.
- Files. Paste, drop anywhere with globalDrop, or pick from the + menu. Images get previews, uploads show progress and can be retried or cancelled, and limits reject files with a reason you can show.
- Long pastes. Text longer than pasteLimit becomes a “Pasted text” attachment with a line count, which can be previewed or put back inline.
- Mentions and commands. A menu opens at the caret for any trigger. @ mentions become chips, / commands insert text, and the list filters and highlights as you type.
- Model, effort and context. A model picker with 1–9 shortcuts, an effort slider with haptics that comes alive at Max, an extended-thinking switch, and a ring that shows how full the context window is.
- Dictation. A live waveform while recording, then the transcript lands in the field and can send itself. Uses the browser’s speech recognition, or your own model through transcribe.
- Queue. Messages sent while a reply streams wait in a queue, can be sent now or removed, and go out in order when the reply ends.
- Drafts. With draftKey, unsent text survives reloads and is cleared once sent.
- Up to edit. Press ↑ in an empty field to bring back your last message, through onEditLast.
Add the Pro registry to components.json
components.json Add your token
Create a token on your account page and put it in
.env.localasHEXTAUI_PRO_TOKEN.Add the block
pnpm dlx shadcn@latest add @hextaui-pro/prompt-input
With the AI SDK
Pass useChat’s status and stop. Files arrive as FileUIPart with a data URL, and a message typed while a reply streams is queued and sent when it finishes.
Model, effort and dictation
Put the model picker, effort slider and context ring under the composer with PromptInputMeta. Turn tools on from the + menu and show them as chips. Pass transcribe to send recordings to your own speech model, and autoSend to send as soon as the text lands.
Mentions and commands
Add a menu per trigger. @ turns a pick into a chip above the text, and / inserts the command. Arrow keys move, Enter or Tab picks and Escape closes, all without leaving the text.
Uploading files
Give upload a function that reports progress and honours the abort signal. Throw UploadError with retryable: false for errors a retry can’t fix, like a file that’s too large for the server.
Anatomy
The parts you compose, from the outside in.
| Part | Description |
|---|---|
PromptInput | The form and shared state: text, files, mentions, queue and status. |
PromptInputBody | The rounded surface that changes between compact and expanded. |
PromptInputTextarea | The field. All keyboard handling lives here. |
PromptInputTools | Left side: the + menu and tool chips. |
PromptInputActions | Right side: dictation and send. |
PromptInputSubmit | Send, stop or queue, depending on status and text. |
PromptInputAttachments | File and mention chips above the text. |
PromptInputMenu | A caret menu for one trigger character. |
PromptInputMeta | The row under the composer for the model, effort and context controls. |
PromptInputQueue, PromptInputSuggestions, PromptInputMeter | Queued messages, starter prompts and a character meter near maxLength. |
PromptInput
Also accepts every form prop except onSubmit and defaultValue.
| Prop | Type | Default |
|---|---|---|
onSubmitReceives { text, files, mentions }. Return a promise to keep the text until it resolves. | (message: PromptMessage) => void | Promise<unknown> | – |
statusPass useChat’s status. While busy, send becomes stop or queue. | "ready" | "submitted" | "streaming" | "error" | "ready" |
onStopCalled from the stop button or Escape. | () => void | – |
onEditLastReturn the last message to load it with ↑ in an empty field. | () => string | void | – |
valueControlled text. | string | – |
defaultValueInitial text. | string | "" |
onValueChangeCalled as the text changes. | (value: string) => void | – |
submitOnWhich key sends. ⌘↵ always sends. | "enter" | "mod+enter" | "enter" |
pasteLimitCharacters before a paste becomes an attachment. | number | false | 5000 |
maxLengthHard limit, shown by PromptInputMeter. | number | – |
draftKeySaves unsent text to local storage under this key. | string | – |
globalDropAccept files dropped anywhere on the page. | boolean | false |
focusOnTypeTyping while nothing else has focus starts typing here. | boolean | false |
layoutForce a layout instead of switching. | "auto" | "compact" | "expanded" | "auto" |
acceptAccepted file types, like the input attribute. | string | – |
maxFilesMost files at once. | number | – |
maxFileSizeLargest file in bytes. | number | – |
uploadUploads each file. Throw UploadError with retryable: false for permanent failures. | (file, { signal, onProgress }) => Promise<unknown> | – |
onRejectCalled for files over a limit or of the wrong type. | (file: File, reason: string) => void | – |
disabledDisables the whole composer. | boolean | false |
| Prop | Type | Default |
|---|---|---|
triggerThe character that opens it, such as @ or /. | string | – |
items{ value, label, description?, icon?, hint?, keywords?, insert? } | PromptMenuItem[] | – |
modeMentions add a chip; text inserts insert ?? label. | "mention" | "text" | – |
labelSection label at the top of the menu. | string | – |
limitMost rows shown. | number | – |
onSelectCalled after a pick. | (item: PromptMenuItem) => void | – |
| Prop | Type | Default |
|---|---|---|
modelsThe first nine get 1–9 shortcuts. | { value, label }[] | – |
valueSelected model. | string | – |
onValueChangeCalled on pick. | (value: string) => void | – |
| Prop | Type | Default |
|---|---|---|
levelsFrom least to most. | { value, label }[] | – |
valueSelected level. | string | – |
onValueChangeCalled as the slider moves. | (value: string) => void | – |
recommendedMarks one level as recommended. | string | – |
thinkingShows the extended-thinking switch when set. | boolean | – |
onThinkingChangeCalled from the switch. | (thinking: boolean) => void | – |
thinkingLabelSwitch label. | string | – |
descriptionHelp text in the tooltip. | string | – |
| Prop | Type | Default |
|---|---|---|
usedTokens used. | number | – |
limitContext window size. | number | – |
limitsExtra rows, such as plan usage. | { label, used, detail? }[] | – |
limitsLabelHeading for those rows. | string | – |
| Prop | Type | Default |
|---|---|---|
transcribeSend the recording to your speech model. Without it, the browser’s speech recognition is used. | (audio: Blob) => Promise<string> | – |
shortcutStarts and finishes a recording from anywhere. | string | false | "ctrl+shift+d" |
autoSendSend as soon as the transcript lands. | boolean | false |
| Prop | Type | Default |
|---|---|---|
iconShown in the chip, and replaced by × on hover. | ReactNode | – |
childrenTool name. | string | – |
toneChip color. | "blue" | "green" | "amber" | "violet" | "pink" | "blue" |
onRemoveCalled when the chip is clicked. | () => void | – |
| Key | Action |
|---|---|
| Enter | Sends. On touch screens it adds a line instead. |
| ShiftEnter | Adds a line. |
| ⌘Enter | Always sends, even when submitOn is mod+enter. |
| Esc | Stops a streaming reply, closes a menu, or cancels a recording. |
| ↑ | In an empty field, brings back your last message when onEditLast is set. |
| ↑↓ | Moves through an open @ or / menu. |
| EnterTab | Picks the highlighted menu row. |
| 1–9 | Picks a model while the model menu is open. |
| ←→ | Moves the effort slider. Home and End jump to the ends. |
| CtrlShiftD | Starts dictation, and inserts the transcript when pressed again. |
- The field has a visible placeholder and an accessible name, and every icon button has a label and tooltip with its shortcut.
- The caret menu is a combobox with aria-activedescendant, so focus never leaves the text while moving through it.
- Uploads, rejections, queue changes and dictation are announced through a polite live region.
- The effort control is a slider with a value text, and it keeps working with arrow keys and Home and End.
- Inputs are at least 16px on touch screens, so iOS never zooms in on focus.
- With reduced motion, height changes, the waveform and the effort fill all settle without animating.
Code
8 files, added to components/blocks/prompt-input.