Skip to content
ComposerPrompt inputMenu

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.

Attachments, context, tools, model and effort, dictation, and Send that becomes Stop.

Installation

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/prompt-input.json

Usage

tsx
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.

Pasted images and long text become files; unsupported types are refused with a reason.

States

Empty, ready, sending, streaming, disabled and long text.

Floating

The one place translucency is allowed: chrome floating over content. It falls back to a solid surface where backdrop-filter is unsupported.

Translucent composer over content, solid where blur is unsupported.

Minimal

One row: attach, text and send inline.

Hero

Larger type and room for empty states.

Props

PropTypeNotes
value, defaultValue, onValueChangestringControlled or uncontrolled text
onSubmit({ text }) => voidNot called while empty, disabled or busy
status"idle" | "submitting" | "streaming"Streaming shows Stop and enables Escape
onStop() => voidStop button and Escape
disabledbooleanRead-only, rate limited or offline. Say why in the placeholder
variant"default" | "floating" | "minimal" | "hero"
onFiles(files, source) => voidsource is paste, drop or picker. Enables drop and the attach button
onFilesRejected(rejections) => voidEach with reason: type, size or count
accept, maxFileSize, maxFilesSame syntax as <input accept>; bytes
pasteTextAsFileAtnumberCharacters. Off by default
hasContentbooleanAllow 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
clearOnSubmitbooleanUncontrolled only. Default true
dropLabel, dropHintReactNodeDrop overlay copy
PartNotes
PromptInputTextareaGrows to 40% of the viewport, then scrolls. 16 px on touch screens so iOS never zooms
PromptInputHeaderSlot above the text for ContextChips
PromptInputAttachmentsSlot above the text for an AttachmentList
PromptInputToolbarThe row under the text
PromptInputTools, PromptInputActionsLeading group that shrinks, trailing group that never does
PromptInputButtonGhost toolbar button with tooltip and shortcut
PromptInputAttachButtonOpens the file picker. Hidden without onFiles
PromptInputSubmitSendButton wired to the composer

Keyboard

KeyAction
EnterSend. Never while an IME is composing
Shift+EnterNew line
Cmd+Enter, Ctrl+EnterSend, in every submitOn mode
EscStop the response while streaming, from anywhere in the composer
Click on the surfacePuts 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 229 are 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.