Prompt Input
The AI composer: an auto-growing textarea with a built-in toolbar and a send or stop button. Wire onSubmit once and the textarea, model picker, count and submit stay in sync through context.
The bar is built out of our Button: an attach menu, the model picker, and a single tools dropdown (image, canvas, web / deep research, code) on the left, dictate and send on the right. Only one tool runs at a time, so the menu is single-select: picking one clears the others. The trigger names what's on, showing the active tool's icon and label rather than a generic Tools word. Every toolbar glyph renders at a consistent 20px, and the send button is an upward arrow that flips to a stop square while a response streams.
Installation#
Usage#
import {
PromptInput,
PromptInputTextarea,
PromptInputToolbar,
PromptInputButton,
PromptInputSubmit,
} from "@/components/ui/prompt-input"
export function Composer() {
return (
<PromptInput onSubmit={(value) => sendMessage(value)}>
<PromptInputTextarea />
<PromptInputToolbar>
<PromptInputSubmit />
</PromptInputToolbar>
</PromptInput>
)
}The composer is uncontrolled by default: it owns its value and clears itself after a submit. Enter sends, Shift+Enter inserts a newline, and the field grows to fit its content up to a cap, then scrolls.
Minimal#
Same capabilities, quieter chrome: each group becomes its own compact icon dropdown (attachments, model, tools) so the bar is just icons plus send. Models are a radio group; the tools (image, canvas, web / deep research, code) are mutually exclusive, so picking one clears the rest (at most one active; their check sits on the right, the menu convention). Each trigger is a static icon-only PromptInputButton.
Sizes#
Three sizes retune the field's padding, text, and min-height, and the send button scales with them. The surface radius stays constant (a brand trait). Set it once on the root with size.
Streaming (send ⇄ stop)#
Pass loading while a response streams and the send button becomes a stop button that fires onStop. The toolbar holds plain PromptInputButton actions (Search, Reason) on the left; the submit claims the right edge automatically.
Suggestions#
Prompt-starter chips sit above the composer, filled in the secondary color so the row reads as one quiet set. Give each its own 20px glyph (not a generic sparkle) and let the shape do the differentiating. They're context-free on purpose: wire each onClick to set the prompt, submit, or both.
The row is a single line that scrolls, never one that wraps. A starter is usually a whole question, so on a phone even two of them are wider than the screen, and a chip cannot give width back: wrapped, the second one simply ran off the edge. Scrolled, the edge fades toward the starters you have not seen yet, and the fade is inert whenever they already fit.
Banner#
A notice that tucks behind the composer for model availability, usage caps, or errors. The composer stays a complete rounded card on top and the banner slides under it, so only its outer edge peeks out. Drop the PromptInputBanner as a sibling of PromptInput in a flex flex-col isolate wrapper (the isolate keeps its negative z local), and add shadow-md to the composer so its shadow falls on the strip. side picks which edge it peeks from: top (banner before the composer) or bottom (banner after). No absolute positioning. tone tints the icon (default / info / warning / destructive); pass onClose for a dismiss button.
Move it below with side="bottom" for a promo (a "connect your apps" nudge, say). Same wrapper, but place the banner after the composer so it peeks out the bottom instead.
Character count#
Drop a PromptInputCount into the toolbar: it reads the composer's length from context (pass current to show tokens instead). With a max it renders current / max and turns destructive once exceeded.
API reference#
PromptInput
Root <form> + context provider. Forwards all native form props.
value? / defaultValue?: controlled or initial value. Uncontrolled clears itself after a submit.onValueChange?(value): called on every keystroke.onSubmit?(value): fired on Enter or send; skipped when empty, disabled, or loading.onStop?(): fired by the stop button.loading?: a response is streaming; flips send → stop.disabled?: dims and freezes the composer.
PromptInputTextarea
Auto-growing field. Forwards all native <textarea> props except value / onChange (owned by the root).
submitOnEnter?: Enter submits (default). Setfalseso Enter adds a line and ⌘/Ctrl+Enter sends.
PromptInputSubmit
The send⇄stop button: a Button under the hood, so it takes all Button props (variant, size, …). Its send glyph is an upward arrow by default (it flips to a stop square while loading); pass children to override it. Custom sendLabel / stopLabel set the accessible names.
PromptInputButton · PromptInputToolbar · PromptInputCount
PromptInputButton is a ghost Button for toolbar actions (its icons render at a consistent 20px across the bar); PromptInputToolbar is the bottom bar; PromptInputCount takes current? / max?.
PromptInputBanner · PromptInputBannerAction
A second card that sits behind the composer, plus an inline link-styled PromptInputBannerAction. side (top / bottom) picks the edge it peeks from; tone (default / info / warning / destructive) tints the leading icon; icon overrides or drops (false) the glyph; onClose / closeLabel add a dismiss button.
PromptInputSuggestions · PromptInputSuggestion
Context-free chip row + chip, meant to sit above the composer. The chip forwards all native <button> props; wire onClick yourself.