Prompt Input
ProThe 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#
Usage#
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.
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.
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.
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.
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.
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.
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.
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.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | - | The prompt, controlled. Reset it yourself to clear after a send. |
| defaultValue | string | - | 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. |
| loading | boolean | false | A response is streaming: send flips to stop. |
| disabled | boolean | false | Dims and freezes the composer. |
| maxLength | number | - | 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 attribute | Description |
|---|---|
| data-disabled | Present when disabled. |
| data-loading | Present 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.
| Prop | Type | Default | Description |
|---|---|---|---|
| submitOnEnter | boolean | true | Enter sends and Shift+Enter breaks a line. false: Enter breaks, ⌘ or Ctrl+Enter sends. |
| placeholder | string | "Ask anything…" | The empty field's hint. |
| maxLength | number | - | 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.
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | ButtonProps["variant"] | "ghost" | The button's style. |
| size | ButtonProps["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.
| Prop | Type | Default | Description |
|---|---|---|---|
| children | React.ReactNode | - | The send glyph; an upward arrow by default. |
| variant | ButtonProps["variant"] | "primary" | The button's style. |
| size | ButtonProps["size"] | "sm" | 32px, the height every control on the toolbar shares. |
| sendLabel | string | "Send message" | The send state's accessible name. |
| stopLabel | string | "Stop generating" | The stop state's accessible name. |
PromptInputCount
A character readout, aria-live="polite". Forwards native <span> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| current | number | - | Overrides the composer's character count, e.g. with tokens. |
| max | number | - | A limit: renders current / max and turns destructive past it. Defaults to the composer's maxLength. |
| threshold | number | 0 | Shows 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 attribute | Description |
|---|---|
| data-over | Present 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.
| Prop | Type | Default | Description |
|---|---|---|---|
| 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. |
| icon | React.ReactNode | false | - | Overrides the tone's icon; false drops it. |
| onClose | () => void | - | Adds a dismiss button that calls it. |
| closeLabel | string | "Dismiss" | The dismiss button's accessible name. |
| Data attribute | Description |
|---|---|
| data-tone | The banner's tone. |
| data-side | The 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.
| Prop | Type | Default | Description |
|---|---|---|---|
| label* | string | - | The tooltip and the button's accessible name, e.g. Connect Slack. |
| src | string | - | 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.
| Prop | Type | Default | Description |
|---|---|---|---|
| label | React.ReactNode | - | A quiet caption above the rows ("Start with"); also names the group for screen readers. |
| visible | number | - | How many rows show before the rest fold behind the toggle. Omit to show them all. |
| moreLabel | (hidden: number) => React.ReactNode | (n) => Show ${n} more | The toggle's label while folded, given the hidden count. |
| lessLabel | React.ReactNode | "Show less" | The toggle's label while unfolded. |
| expanded | boolean | - | Unfolded, controlled. |
| defaultExpanded | boolean | false | Unfolded 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.
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | ButtonProps["variant"] | "ghost" | The button's style. |
| size | ButtonProps["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.
| Prop | Type | Default | Description |
|---|---|---|---|
| trigger | string | "@" | The character that opens the list. |
| maxSpaces | number | 0 | Spaces 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. |
| selectOnTab | boolean | true | Tab 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.
| Prop | Type | Default | Description |
|---|---|---|---|
| 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. |
| label | string | "Suggestions" | The listbox's accessible name. |
| container | HTMLElement | null | document.body | The 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. |
| sideOffset | number | 4 | The gap from the anchor, in pixels. |
| Data attribute | Description |
|---|---|
| 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.
| Prop | Type | Default | Description |
|---|---|---|---|
| value* | string | - | The row's unique id (a user id, a handle), reported in onMentionSelect. |
| textValue | string | - | What gets written after the trigger when picked. Defaults to value. |
| disabled | boolean | false | Skips 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 attribute | Description |
|---|---|
| data-active | Present on the highlighted row. |
| data-disabled | Present 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.