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#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add prompt-input

Usage#

usage.tsxTSX
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.

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.

Fable 5 is unavailable right now.

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.

Get better answers from your apps.

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.

54 / 500

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). Set false so 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.

FAQ#

Uncontrolled: it owns its value and clears itself after a submit. Pass `value` plus `onValueChange` to control it, in which case you reset the value yourself to clear it.