Prompt Input

Pro

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 16px on a 32px control, and the send button, the same size as its neighbours, 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# Activate your Pro license (once per machine)npx koalaui-cli@latest login <key># Add this component (its dependencies come along)npx koalaui-cli@latest add prompt-input

Usage#

This example is part of Koala UI Pro

The preview is yours to explore. Its code comes with a Pro license, paid once and used in every project.

Have a license? Unlock it here

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.

Anatomy#

import {  PromptInputSuggestions,  PromptInputSuggestion,  PromptInputQuickActions,  PromptInputQuickAction,  PromptInputBanner,  PromptInputBannerAction,  PromptInput,  PromptInputMentions,  PromptInputTextarea,  PromptInputMentionList,  PromptInputMentionItem,  PromptInputMentionEmpty,  PromptInputToolbar,  PromptInputMentionButton,  PromptInputButton,  PromptInputCount,  PromptInputSubmit,} from "@/components/ui/prompt-input"<PromptInputSuggestions>  <PromptInputSuggestion /></PromptInputSuggestions><PromptInputQuickActions>  <PromptInputQuickAction /></PromptInputQuickActions><div className="relative isolate flex flex-col">  <PromptInputBanner>    …    <PromptInputBannerAction />  </PromptInputBanner>  <PromptInput>    <PromptInputMentions>      <PromptInputTextarea />      <PromptInputMentionList>        <PromptInputMentionItem />        <PromptInputMentionEmpty />      </PromptInputMentionList>      <PromptInputToolbar>        <PromptInputMentionButton />        <PromptInputButton />        <PromptInputCount />        <PromptInputSubmit />      </PromptInputToolbar>    </PromptInputMentions>  </PromptInput></div>

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.

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 16px 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.

Connect your apps#

Instead of a single action, the strip can carry the apps themselves: PromptInputBannerApps holds a row of PromptInputBannerApp logos, each a button named by a tooltip. The banner's muted tint is what makes it read as sitting behind the composer, and when the row outgrows the strip it scrolls under a soft fade instead of wrapping.

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

Character limit#

Set maxLength once on the root. The textarea enforces it (typing and pasting stop at the cap), a PromptInputCount reads it as its max, and a value over it (one set from outside) cannot be sent. A count that is always on is noise; give it a threshold (a fraction of the limit) and it appears only near the cap. Here it shows from 80% on: type a few more words.

96 / 120

Quick actions#

PromptInputSuggestions is a scrolling row of question chips. When the starters are actions (an icon and a verb), stack them as a list instead: PromptInputQuickActions takes an optional label that names the group, and visible folds the rest behind a “Show N more” toggle (a Collapsible: it carries aria-expanded, the rows tween open and the label rolls). Each PromptInputQuickAction is a ghost Button row whose onClick you own: the pattern below writes the full prompt into the composer instead of sending it, so the reader can edit it first.

Start with

Mentions#

Wrap the textarea and a PromptInputMentionList in PromptInputMentions. Typing @ at the start of a word (or pressing PromptInputMentionButton) opens a list right at the line being typed: a Radix Popover anchored to the caret, flipping when there is no room. The query flows out through onQueryChange (filter or fetch with it), and picking a row replaces @query with @Name and fires onMentionSelect.

Focus never leaves the textarea. It controls the listbox (aria-controls, aria-haspopup) and names the highlighted row with aria-activedescendant. ↑ ↓ move, Enter and Tab pick (selectOnTab={false} keeps Tab for leaving), and Escape closes only the list: it is caught before a dialog or a panel that closes on Escape can see it. It is a plain textarea, no contenteditable, so it behaves the same in every engine, older embedded Chromium included.

↑ ↓ to move, Enter or Tab to pick, Esc to close the list.

API reference#

PromptInput

The composer: a <form> that owns the value and shares it with its parts. Forwards native <form> props but onSubmit. Every part accepts className, merged last.

PropTypeDefaultDescription
valuestring-The prompt, controlled. Reset it yourself to clear after a send.
defaultValuestring-The initial prompt. Uncontrolled, the field clears itself after a submit.
onValueChange(value: string) => void-Called on every keystroke.
onSubmit(value: string) => void-Called on Enter or the send button; skipped while empty, disabled or loading.
onStop() => void-Called by the stop button.
loadingbooleanfalseA response is streaming: send flips to stop.
disabledbooleanfalseDims and freezes the composer.
maxLengthnumber-A character cap, set once here: the textarea enforces it natively (typing and pasting stop at it), PromptInputCount reads it as its max, and a value over it cannot be sent.
Data attributeDescription
data-disabledPresent when disabled.
data-loadingPresent while loading.

PromptInputTextarea

The field; grows with its content up to a cap, then scrolls. Forwards <textarea> props but value, defaultValue and onChange, which the root owns.

PropTypeDefaultDescription
submitOnEnterbooleantrueEnter sends and Shift+Enter breaks a line. false: Enter breaks, ⌘ or Ctrl+Enter sends.
placeholderstring"Ask anything…"The empty field's hint.
maxLengthnumber-The native cap; defaults to the composer's maxLength, so set it on the root instead.

PromptInputToolbar

The bottom bar for actions and the send button. Forwards native <div> props.

PromptInputButton

A toolbar action (attach, model picker, mic): a ghost Button sized to the composer, so its icons line up across the bar. Forwards Button props.

PropTypeDefaultDescription
variantButtonProps["variant"]"ghost"The button's style.
sizeButtonProps["size"]"sm"32px, the height every control on the toolbar shares.

PromptInputSubmit

The send button, disabled while the prompt is empty or over maxLength; a stop button calling onStop while loading. Pinned right. Forwards Button props but iconOnly and asChild.

PropTypeDefaultDescription
childrenReact.ReactNode-The send glyph; an upward arrow by default.
variantButtonProps["variant"]"primary"The button's style.
sizeButtonProps["size"]"sm"32px, the height every control on the toolbar shares.
sendLabelstring"Send message"The send state's accessible name.
stopLabelstring"Stop generating"The stop state's accessible name.

PromptInputCount

A character readout, aria-live="polite". Forwards native <span> props.

PropTypeDefaultDescription
currentnumber-Overrides the composer's character count, e.g. with tokens.
maxnumber-A limit: renders current / max and turns destructive past it. Defaults to the composer's maxLength.
thresholdnumber0Shows the count only near the limit: the fraction of max (0 to 1) it must reach first, e.g. 0.8. Below it the part renders nothing. Needs a max.
Data attributeDescription
data-overPresent once the count passes max.

PromptInputBanner

A second card peeking from behind the composer (limits, errors). Place it beside PromptInput in a relative isolate flex flex-col wrapper. Forwards native <div> props.

PropTypeDefaultDescription
side"top" | "bottom""top"The edge it peeks from: render it before the composer for top, after for bottom.
tone"default" | "info" | "warning" | "destructive""default"Tints the leading icon.
iconReact.ReactNode | false-Overrides the tone's icon; false drops it.
onClose() => void-Adds a dismiss button that calls it.
closeLabelstring"Dismiss"The dismiss button's accessible name.
Data attributeDescription
data-toneThe banner's tone.
data-sideThe banner's side.

PromptInputBannerAction

An inline link-styled action in a banner, e.g. Learn more. Renders a <button type="button">.

PromptInputBannerApps

A row of app logos at the end of a banner; scrolls with a fade when it outgrows the strip. A role="group" <div>: give it an aria-label.

PromptInputBannerApp

One app in the row: its logo as a button, named by a tooltip. Forwards native <button> props.

PropTypeDefaultDescription
label*string-The tooltip and the button's accessible name, e.g. Connect Slack.
srcstring-The logo's URL. Or pass your own SVG as children.

PromptInputSuggestions

A row of prompt-starter chips above the composer; context-free. Forwards native <div> props.

PromptInputSuggestion

One chip. Renders a <button type="button">; wire onClick yourself (fill the prompt, send, or both).

PromptInputQuickActions

A labelled list of starter actions above the composer while the thread is empty, the vertical sibling of PromptInputSuggestions; the tail can fold behind a "Show N more" Collapsible. A role="group"; forwards native <div> props.

PropTypeDefaultDescription
labelReact.ReactNode-A quiet caption above the rows ("Start with"); also names the group for screen readers.
visiblenumber-How many rows show before the rest fold behind the toggle. Omit to show them all.
moreLabel(hidden: number) => React.ReactNode(n) => Show ${n} moreThe toggle's label while folded, given the hidden count.
lessLabelReact.ReactNode"Show less"The toggle's label while unfolded.
expandedboolean-Unfolded, controlled.
defaultExpandedbooleanfalseUnfolded on first render, uncontrolled.
onExpandedChange(expanded: boolean) => void-Called when the toggle folds or unfolds the rows.

PromptInputQuickAction

One quick action row: a ghost Button laid out full width and left aligned, icon muted. Forwards Button props; wire onClick yourself.

PropTypeDefaultDescription
variantButtonProps["variant"]"ghost"The button's style.
sizeButtonProps["size"]"sm"The button's size.

PromptInputMentions

Adds @ mentions to the composer: wrap the PromptInputTextarea, a PromptInputMentionList and the toolbar with it, inside a PromptInput. Typing the trigger at the start of a word opens the list; picking a row replaces @query with @text . Escape closes only the list.

PropTypeDefaultDescription
triggerstring"@"The character that opens the list.
maxSpacesnumber0Spaces allowed in the query before the list gives up: 0 for handles, 1 for "First Last" names.
onQueryChange(query: string | null) => void-Called as the query changes, with the text after the trigger, or null once the list closes. Filter or fetch with it.
onMentionSelect(mention: PromptInputMention) => void-Called when a row is picked, as its text is written into the composer.
selectOnTabbooleantrueTab picks the highlighted row. false keeps Tab for leaving the field.

PromptInputMentionList

The mention list: a compact Popover surface holding a listbox, opening above the line and flipping when there is no room. Focus never leaves the textarea. Forwards Radix Popover.Content props but asChild.

PropTypeDefaultDescription
anchor"caret" | "field""caret"What the list hangs from: the line box of the @ being typed, or the whole textarea so it spans the composer's width.
labelstring"Suggestions"The listbox's accessible name.
containerHTMLElement | nulldocument.bodyThe portal target, e.g. an app frame scaled with a transform.
side"top" | "right" | "bottom" | "left""top"The preferred side of the anchor.
align"start" | "center" | "end""start"Alignment against the anchor.
sideOffsetnumber4The gap from the anchor, in pixels.
Data attributeDescription
data-anchor"caret" or "field".
data-state"open" or "closed", from Radix.

PromptInputMentionItem

One row of the list (role="option"); lay out an avatar, a name and a muted id inside. Forwards native <div> props but onSelect.

PropTypeDefaultDescription
value*string-The row's unique id (a user id, a handle), reported in onMentionSelect.
textValuestring-What gets written after the trigger when picked. Defaults to value.
disabledbooleanfalseSkips the row in keyboard navigation and blocks picking it.
onSelect(mention: PromptInputMention) => void-Called when this row is picked, before the list-wide onMentionSelect.
Data attributeDescription
data-activePresent on the highlighted row.
data-disabledPresent when disabled.

PromptInputMentionEmpty

Shown only while the list has no rows ("No matches", "Searching…"); a polite live region. Forwards native <div> props.

PromptInputMentionButton

A toolbar button that writes the trigger at the caret and opens the list. A PromptInputButton, so it sizes with the composer; an @ glyph and the label "Mention" by default. Must live inside PromptInputMentions.

usePromptInputMentions

A hook for parts inside PromptInputMentions. Returns { open, query, close, insertTrigger }: whether the list is open, the query after the trigger (e.g. to show "Searching…"), and the two actions.

findMentionQuery

The pure helper behind the list: findMentionQuery(text, caret, trigger = "@", maxSpaces = 0) returns { start, end, query } for an open mention ending at the caret, or null.

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.