DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
ChatBubbleStreamingResponseCitationsToolCallPlanListParameterDiffModelSelectorElicitationArtifactPanelPromptInputKeyboardToolbar
AI agentsContributing
  1. Components
  2. PromptInput

PromptInput

AI chat input with auto-grow textarea, send/stop swap, a "+" options menu, @-mention and /-command typeahead, model selector, suggestion chips, drag-drop attachments, voice controls, inline & stacked layouts, RTL and a loading skeleton.

$ pnpm add @tessinaui/ui

When to use

  • A conversational surface. The composer is an auto-growing message field with the controls that send, attach to and configure one turn.
  • Report generation state. Set isGenerating for the whole time a response is streaming: the Stop button is the only way to cancel, and its transitions are what get announced.
  • Set maxLength only when the limit is real. Past it, sending is refused by key and by button, which is a hard stop for the user.
  • Keep the controls to this turn. Attach, model, modes and voice configure what is sent now; anything else belongs elsewhere on the page.
  • Wire the work yourself. The composer never uploads a file, transcribes audio or calls a model. Report each result back through props such as attachment status, and it renders and announces that state.

When not to use

  • For a plain multi-line form field. Use Textarea.
  • For a search field with a submit affordance. Use Search.
  • For a command palette over actions. Use Command.
  • For the message bubbles themselves. Use ChatBubble.
  • For a standalone drop zone with a file list. Use FileUpload.
  • Don't wrap the controls in role="toolbar", and don't rename a toggle when it flips ("Voice input" to "Stop voice input"). A constant name with state on aria-pressed is the toggle contract screen readers rely on.
  • Don't set tone="on-color" on a neutral background. It derives its ink from currentColor, so it needs a real coloured plate, usually a Surface.

Installation

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import { PromptInputBar } from "@tessinaui/ui";
example.tsx
<PromptInputBar
  placeholder="Ask anything…"
  onSend={(value, attachments) => console.log(value, attachments)}
/>

The preview above and the Default example are the composed form: PromptInput with rounded="full" and the parts placed by hand. Reach for PromptInputBar when the props cover your toolbar.

The composer is a controlled display surface: it never uploads a file, calls a model, or streams a response. You report state to it (isGenerating, attachment status) and it renders and announces that state.

Examples

Default

The prompt box of an AI app, composed from the parts: a placeholder that opens / commands and @ context, a plus menu, two tool toggles (web search and extended thinking), a model pill that carries its level, dictation and a round Send. The toolbar answers to the box's own width: narrower, the two toggles fold into one modes menu, and then into the plus menu. Every part is yours to move or drop; PromptInputBar, below, is the same box from props when you do not need that control.

Inline layout

One row — textarea and controls share a line. rounded="full" caps at the panel radius (24px) here too: an inline bar wraps to a second row as soon as it carries a model pill and a couple of toggles, and a pill radius on a wrapped box reads as broken rather than round.

Toolbar controls

Model selector

Modes

The modes a prompt runs with: quiet icon toggles while the box is wide, one menu of switch rows as it narrows, then rows in the plus menu. The menu stays open while several modes flip, and a toggle never changes its name with its state.

Select pills

The pill is a generic single-select, not a model-only control — the assistants use the same shape for an answer mode, a scope and a model. Give an option a meta value for a second axis on the trigger (<model name> + "Extended"), an icon to prefix it, and pass children to append settings rows to the menu.

In PromptInputBar's stacked layout the pill sits in the row of leading controls, which scrolls when they do not fit, so the pill keeps its label and the row scrolls. In a toolbar you compose yourself nothing scrolls, so a crowded toolbar truncates the label instead. On a touch screen the pill's whole width is a 44px target.

Suggestions

Mentions and commands

Type @ to mention someone or / for a command. The list opens where the character starts a word, filters as you type, and inserts plain text, so the value stays a string. Here @ filters a fixed list and / loads its entries from a function.

Attachments

Drag a file onto the bar, paste an image, or use the paperclip. Report transfer state per file with status, progress and errorMessage, and describe an image with alt. A file that breaks accept, maxFiles or maxFileSize stays in the list as an error card that states the rule.

Uploads, cancel and retry

With onAttachmentCancel, the × on an uploading file cancels it. With onAttachmentRetry, a failed upload offers Retry. You run the upload; the composer shows its state.

Voice recording bar

Tap the mic. The composer swaps to a recorder with a live level meter, and Confirm reports the length. The capture here is simulated.

Generating and stopping

While isGenerating is set, Send becomes Stop. Give Stop a progress (0–100) and a ring around it fills as the reply arrives: your own measure, such as tokens received against an expected length or steps done of a plan. Without progress there is no ring. The ring sits where the focus ring would, and a focused Stop moves it one ring further out so the two never overlap.

Submit shortcut

Character counter

Banner

Labelled send

On a coloured surface

On a plate the composer's well is the only wash under text: attachment cards are hairlines inside it, and an open toolbar control — or the modes trigger while any mode is on — becomes the plate's inverse chip.

RTL

Skeleton

Compound API

Compose the parts yourself when the bar's slots are not the arrangement you want.

APIs

Two APIs are available depending on how much control you need.

APIWhen to use
PromptInputBarDrop-in single component for the 90% use case
PromptInput + sub-componentsFull control over toolbar layout and custom actions

PromptInputBar

The batteries-included compound. Manages value and attachment state, auto-clears on send, and composes every sub-component behind opt-in props.

example.tsx
<PromptInputBar
  placeholder="Message…"
  showMenu
  showVoice
  models={MODELS}
  suggestions={["Summarize", "Translate", "Explain"]}
  accept="image/*,.pdf"
  maxLength={500}
  isGenerating={streaming}
  onSend={(value, files) => send(value, files)}
  onStop={() => abort()}
/>

Compound API

Use PromptInput as the root and compose sub-components for full layout control.

example.tsx
import {
  PromptInput,
  PromptInputAttachments,
  PromptInputTextarea,
  PromptInputToolbar,
  PromptInputMenu,
  PromptInputModelSelect,
  PromptInputSpacer,
  PromptInputVoice,
  PromptInputCounter,
  PromptInputSend,
} from "@tessinaui/ui";

function CustomInput() {
  const [value, setValue] = React.useState("");

  return (
    <PromptInput
      value={value}
      onChange={setValue}
      onSend={(v) => { console.log(v); setValue(""); }}
      accept="image/*,.pdf"
      rounded="xl"
    >
      <PromptInputAttachments />
      <PromptInputTextarea placeholder="Message…" maxRows={6} />
      <PromptInputToolbar>
        <PromptInputMenu />
        <PromptInputModelSelect models={MODELS} defaultValue="sonnet-4-6" />
        <PromptInputSpacer />
        <PromptInputVoice />
        <PromptInputCounter />
        <PromptInputSend />
      </PromptInputToolbar>
    </PromptInput>
  );
}

Layout

layout="stacked" (default) places the toolbar in a row beneath the textarea. layout="inline" collapses the textarea and controls onto a single row — the compact assistant-style pill. In the compound API, wrap the row in PromptInputRow.

example.tsx
<PromptInputBar layout="inline" rounded="full" showMenu showVoice />

Modes menu

Pass modes and they render as switch rows (role="menuitemcheckbox") in a dropdown behind a sliders trigger. The menu stays open while several modes flip; the trigger adopts the AA-validated primary tint while any mode is on. For a primary mode that is one-of-many (an answer-mode "Search ▾"), use PromptInputModelSelect instead — it is the generic single-select pill.

example.tsx
<PromptInputBar
  showMenu
  showAttach
  models={MODELS}
  modes={[
    { id: "search", icon: <Globe />, label: "Search" },
    { id: "think", icon: <Lightbulb />, label: "Think" },
  ]}
/>

Options menu

showMenu adds a "+" dropdown. With no menuItems it ships a default AI-tool set (Add photos & files, Create image, Thinking, Deep research, Web search). When the input is attachment-enabled, "Add photos & files" opens the file picker.

example.tsx
<PromptInputBar
  showMenu
  menuItems={[
    { label: "Attach image", icon: <ImageIcon />, onSelect: pickImage },
    { label: "Search the web", icon: <Globe />, onSelect: searchWeb },
  ]}
/>

Model selector

Pass a models list to render a model-picker pill in the toolbar. It is controlled with model / onModelChange, or uncontrolled with defaultModel. Picking an option closes the menu at once, and the chosen row ends in a check. In the compound API, PromptInputModelSelect also takes moreModels for a "More models" submenu, children for a settings switch or a link after the options, and indicator="dot" for a leading dot instead of the check.

example.tsx
const MODELS = [
  { value: "opus-4-7", label: "Opus 4.7", description: "Most capable" },
  { value: "sonnet-4-6", label: "Sonnet 4.6", description: "Balanced" },
  { value: "haiku-4-5", label: "Haiku 4.5", description: "Fastest" },
];

<PromptInputBar models={MODELS} defaultModel="sonnet-4-6" onModelChange={setModel} />

Suggestions

suggestions renders a horizontally-scrollable chip row below the bar. suggestionBehavior="fill" (default) drops the text into the input; "send" submits it immediately. The chips inherit the bar's size and rounded so they stay visually consistent with the input.

example.tsx
<PromptInputBar
  suggestions={["Summarize a document", "Write a function", "Brainstorm ideas"]}
  suggestionBehavior="fill"
/>

Mentions and commands

triggers opens a typeahead at the caret. Each trigger is one character and its entries. Picking an entry replaces the character and the query with plain text.

example.tsx
<PromptInputBar
  triggers={[
    {
      char: "@",
      label: "People",
      items: [{ value: "alice", keywords: ["alice@acme.com"] }, { value: "bob" }],
    },
    {
      char: "/",
      label: "Commands",
      items: (query) => searchCommands(query),
      emptyMessage: "No commands found",
    },
  ]}
/>
  • The value stays plain text. onSend receives "Ask @alice to /summarize", with no token structure. Parse the string if you need one. Picked entries are not drawn as pills.
  • The list opens only where the character starts a word, so typing alice@acme.com never opens it. A space closes it.
  • A function items is called with the query after debounceMs (150 ms by default) and can return a promise. An answer to an older query is dropped.
  • serialize changes the inserted text, which is `${char}${value} ` by default. emptyMessage keeps the list open with a note when nothing matches.

Attachments

Set accept (and optionally maxFiles / maxFileSize) to enable attachments. Files can be added by drag-and-drop, clipboard paste, the "+" menu or the paperclip, which is the keyboard path. Previews render above the textarea, and a message can be sent with attachments only — no text required.

example.tsx
<PromptInputBar
  showAttach
  accept="image/*,.pdf,.txt"
  maxFiles={4}
  maxFileSize={20 * 1024 * 1024}
  onSend={(text, files) => send(text, files)}
  onFilesRejected={(files, rejections) => log(rejections.map((r) => r.message))}
/>

Attachment state can also be controlled with attachments / onAttachmentsChange.

A rejected file stays on screen. It joins the list as an error card whose errorMessage states the rule — "PDF over 20 MB", "CSV files aren't accepted", "Only 4 files allowed" — and carries a rejection code and no file. A rejection card is never sent, never counts toward maxFiles and offers no retry; the user removes it. In controlled mode onAttachmentsChange receives these cards too, so skip anything with rejection when you upload.

Uploads are yours. The composer never uploads: set status: "uploading" with progress while a file transfers, status: "error" with an errorMessage when it fails, and clear status when it is done.

  • onAttachmentCancel(attachment) turns the × on an uploading card into "Cancel upload of <name>". Abort the transfer in the handler; the card leaves the list. Without it, the × only removes.
  • onAttachmentRetry(attachment) shows Retry on a failed card that is not a rejection. Set the attachment back to "uploading" in the handler.
example.tsx
<PromptInputBar
  showAttach
  attachments={attachments}
  onAttachmentsChange={(next) => {
    setAttachments(next);
    next.filter(isNewUpload).forEach(upload);
  }}
  onAttachmentCancel={(a) => controllers.get(a.id)?.abort()}
  onAttachmentRetry={(a) => upload(a)}
/>

Voice & voice mode

showVoice adds a microphone (dictation) button — pass isRecording for the pulsing state. showVoiceMode adds the solid voice-mode button.

example.tsx
<PromptInputBar
  showVoice
  showVoiceMode
  isRecording={recording}
  onVoice={toggleDictation}
  onVoiceMode={openVoiceMode}
/>

Recording bar

Give isRecording a recording handler and the composer becomes a recorder: the textarea and toolbar swap for Cancel, a level meter, the elapsed time and Confirm. The draft is kept and comes back when the bar closes.

Recording is controlled. The composer never opens a microphone — start capture in onVoice, set isRecording once it is really running (permission can fail), and clear it from onRecordingCancel / onRecordingConfirm. Feed recordingElapsed and recordingAmplitude from your own pipeline; without them the bar counts from when it appeared and its bars idle on a fixed shape.

example.tsx
<PromptInputBar
  showVoice
  isRecording={recording}
  onVoice={startCapture}
  recordingElapsed={seconds}
  recordingAmplitude={levels} // 0–1, newest last
  onRecordingCancel={discardCapture}
  onRecordingConfirm={sendVoiceNote}
/>

Without a recording handler, isRecording only pulses the mic, so dictation straight into the textarea keeps working. In the compound API, render PromptInputRecording in place of the textarea and toolbar yourself.

Only tap-to-start ships. Hold-to-record with slide-to-cancel is a gesture with no keyboard path, so it would need a button twin anyway — which is this.

Send / Stop swap

Pass isGenerating={true} to swap the send button for a stop button. The textarea stays editable while generating so the next message can be composed, and both the start and the end of a generation are announced politely.

disabled outranks isGenerating: a disabled composer cannot be stopped from here either.

example.tsx
<PromptInputBar
  isGenerating={isStreaming}
  onSend={(v) => startStream(v)}
  onStop={() => abortStream()}
/>

Character counter

maxLength enables a live counter that turns warning past 80% and error past the limit. Typing past the limit is never blocked — a browser cannot gracefully truncate pasted or dictated text — but sending is refused until the value fits, by key and by button. Screen readers hear the count only once typing pauses, and only after the threshold.

example.tsx
<PromptInputBar maxLength={500} showCounter />

Banner

banner renders a dismissible notice at the top of the bar — handy for usage notes or model warnings. bannerIntent (error, warning, success, info) tints it, and an error or warning notice is also spoken once when it appears.

example.tsx
<PromptInputBar
  banner="Opus consumes usage limits faster than other models."
  bannerIcon={<Sparkles />}
  onBannerDismiss={dismiss}
/>

Skeleton

Render PromptInputSkeleton while the interface loads. It mirrors the bar's size, rounded and layout — every box comes from the maps the real parts read, so the two cannot drift.

It is silent by default: pass label only when this placeholder is the thing that should announce the wait.

example.tsx
<PromptInputSkeleton layout="stacked" />
<PromptInputSkeleton layout="inline" rounded="full" />
<PromptInputSkeleton label="Loading composer" />

RTL

Pass dir="rtl" to PromptInput or PromptInputBar. The layout, toolbar, menu, attachments and suggestion rows all mirror automatically.

Left unset, no dir is stamped at all and the composer inherits the document's direction — so an RTL page needs nothing here.

An explicit dir="rtl" also switches the attachment strings to Arabic: card actions and states, announcements, the rules a rejected file broke, the drop overlay and the Attach button's name. Counts take Arabic plural forms. Pass strings to replace any entry in any language.

example.tsx
<PromptInputBar dir="rtl" placeholder="اكتب رسالتك…" onSend={handleSend} />

Keyboard behaviour

KeyAction
EnterSubmit — calls onSend (when submitOn="enter", the default)
Shift + Enter / Alt + EnterInsert a newline
⌘/Ctrl + EnterSubmit when submitOn="mod-enter"
IME composingEnter never submits mid-composition, in every mode
↑ / ↓ with a typeahead openMove through the entries
Enter / Tab with a typeahead openInsert the active entry. Enter does not send
Escape with a typeahead openClose the list and keep the text
Tab / Shift + TabMove through the toolbar controls — each is its own stop
Space / Enter on a toggleFlip a mode, voice or voice-mode button
Escape in the recording barCancel the recording

The toolbar is deliberately not role="toolbar". That pattern obliges arrow-key navigation and a single tab stop, and the APG explicitly warns against placing an arrow-consuming control — a textbox, which is exactly the inline layout — inside one. Every surveyed kit leaves these as ordinary buttons.

Making Enter a newline on phones. No platform documents Return-to-send on a soft keyboard, and no surveyed system makes touch-newline a default. Opt in yourself with submitOn="none" below md, which leaves the Send button as the only path:

example.tsx
const isPhone = useMediaQuery("(max-width: 767px)");

<PromptInputBar submitOn={isPhone ? "none" : "enter"} onSend={send} />

API Reference

PromptInputBar — core

PropTypeDefaultDescription
valuestring—Controlled value
defaultValuestring""Uncontrolled initial value
onChange(value: string) => void—Fires on every keystroke
onSend(value: string, attachments: PromptInputAttachment[]) => void—Called on Enter or Send; value + attachments auto-clear
onStop() => void—Called when the Stop button is pressed
isGeneratingbooleanfalseSwaps Send for Stop
size"sm" | "md" | "lg""md"Density. Text steps 12 / 14 / 16 on desktop, floored at 16 on phones
rounded"none" | "sm" | "md" | "lg" | "full""lg"Border-radius — applied to the container and every inner control
layout"stacked" | "inline""stacked"Toolbar-below vs single-row layout
disabledbooleanfalseDisables all interactions
dir"ltr" | "rtl"—Text direction. Unset, the composer inherits the page's
tone"default" | "on-color"inheritedOn a coloured or dark plate the well derives its ink from currentColor. Inherited from a wrapping <Surface>, else "default"; an explicit prop wins
submitOn"enter" | "mod-enter" | "none""enter"Which keystroke submits. "none" leaves the button as the only path
placeholderstring"Message…"Textarea placeholder
maxRowsnumber8Lines before the textarea scrolls
maxLengthnumber—Character limit; enables the counter. Typing past it is never blocked, but sending is refused until the value fits
showCounterbooleanfalseForce-show the counter without maxLength
triggersPromptInputTrigger[]—Characters that open a typeahead at the caret, such as @ and /. Inserts plain text

PromptInputBar — toolbar slots

PropTypeDefaultDescription
showMenubooleanfalseShow the "+" options menu
menuItemsPromptInputMenuItem[]default setCustom menu items
showAttachbooleanfalseShow the paperclip attach button
onAttach() => void—Overrides the attach button click
showVoicebooleanfalseShow the microphone button
onVoice() => void—Called when the mic is clicked
isRecordingbooleanfalseRecording is live — pulses the mic and announces start / stop; with a recording handler, shows the recording bar
onRecordingCancel() => void—Discards the recording (Cancel, or Escape in the bar)
onRecordingConfirm() => void—Keeps the recording (Confirm)
recordingElapsednumber—Seconds recorded. Omitted, the bar counts from when it appeared
recordingAmplitudenumber[]—Live levels, 0–1, newest last. Omitted, the bars idle
showVoiceModebooleanfalseShow the solid voice-mode button
onVoiceMode() => void—Called when voice mode is clicked
isVoiceModeActivebooleanfalseActive state on the voice-mode button
modelsPromptInputModelOption[]—Renders the model-selector pill
model / defaultModelstring—Controlled / uncontrolled model value
onModelChange(value: string) => void—Fires when the model changes
modesPromptInputMode[]—Modes (e.g. Search, Think) — switch rows in the sliders menu

PromptInputBar — send, suggestions, banner & attachments

PropTypeDefaultDescription
sendTextReactNode—Visible label on the Send button. Omitted, Send stays icon-only
bannerIntent"none" | "error" | "warning" | "success" | "info""none"Banner meaning. error and warning notices are also spoken through the composer's live region when they appear or change

PromptInputAttachment

FieldTypeDescription
idstringStable identity — used as the React key and in announcements
namestringFile name, shown and announced
sizenumberBytes, formatted for display
typestringMIME type; image/* renders a thumbnail
urlstringObject/data URL for the thumbnail
altstringDescribes an image thumbnail. Unset, the thumbnail is decorative
fileFileThe picked file. Absent on a rejection card
status"uploading" | "error"Transfer state. Absent means settled
progressnumber0–100, shown as a determinate bar while uploading
errorMessagestringShown in place of the size when status is "error"
rejection"file-invalid-type" | "file-too-large" | "too-many-files"Set by the composer on a file it turned away. Never sent, not counted, no retry

PromptInputTrigger

FieldTypeDefaultDescription
charstring—One character. Opens the list where it starts a word
itemsPromptInputTriggerItem[] | (query: string) => PromptInputTriggerItem[] | Promise<PromptInputTriggerItem[]>—The entries, or a function called with the query that does its own filtering
labelstring"Suggestions"Accessible name of the list
minQueryLengthnumber0Characters typed after the trigger before the list opens
debounceMsnumber150Delay before a function items is called
filter(value: string, query: string) => booleansubstringMatch test for array items, checked against value and keywords
serialize(item) => string`${char}${value} `The inserted text
onSelect(item) => void—Called after an entry is inserted
emptyMessagestring—Shown when nothing matches. Unset, the list closes
loadingMessagestring"Loading…"Shown while a function items has not answered

PromptInputTriggerItem

FieldTypeDescription
valuestringInserted after the trigger, and what the query matches
labelReactNodeRow content. Defaults to the trigger plus value
keywordsstring[]More terms the query matches
descriptionstringA quieter second line, outside the option's accessible name
iconReactNodeLeading icon
disabledbooleanShown, but cannot be picked
PropTypeDefaultDescription
suggestionsPromptInputSuggestion[]—Suggestion chips below the bar
suggestionBehavior"fill" | "send""fill"Fill the input vs submit on click
onSuggestionSelect(value: string) => void—Fires when a chip is clicked
bannerReact.ReactNode—Dismissible banner content
bannerIconReact.ReactNode—Leading icon for the banner
onBannerDismiss() => void—Called when the banner is dismissed
acceptstring—Accepted file types; enables attachments
maxFilesnumber—Maximum attachment count
maxFileSizenumber—Maximum size per file, in bytes
attachments / defaultAttachmentsPromptInputAttachment[]—Controlled / uncontrolled attachment list
onAttachmentsChange(attachments: PromptInputAttachment[]) => void—Fires when attachments change
onFilesRejected(files: File[], rejections: PromptInputFileRejection[]) => void—Files rejected by the accept / size / count limits, each with its reason and message
onAttachmentCancel(attachment: PromptInputAttachment) => void—Makes an uploading card's × cancel. Abort the upload here; the card leaves the list
onAttachmentRetry(attachment: PromptInputAttachment) => void—Shows Retry on a failed card that is not a rejection
stringsPartial<PromptInputStrings>English, or Arabic under dir="rtl"Replaces any attachment-flow string
attachablebooleanautoForce-enable drag-drop + paste handling

PromptInput (root)

Accepts all PromptInputBar props except the show* slot-visibility flags and the model / suggestion / banner conveniences. Context is consumed by every sub-component.

PromptInputMenu

PropTypeDefaultDescription
itemsPromptInputMenuItem[]default setMenu items
childrenReact.ReactNode—Custom menu content — overrides items
iconReact.ReactNode<Plus />Trigger icon
side"top" | "bottom""top"Menu open direction
align"start" | "center" | "end""start"Menu alignment
triggerClassNamestring—Classes on the trigger button

PromptInputModelSelect

PropTypeDefaultDescription
modelsPromptInputModelOption[]—Selectable models (required)
value / defaultValuestring—Controlled / uncontrolled selection
onValueChange(value: string) => void—Fires on selection
closeOnSelectbooleantrueClose the menu once an option is picked, so a pick is one click
indicator"check" | "dot""check"How the chosen option is marked: a trailing check, which starts every label at the row's own padding, or a leading dot
menuSize"xs" | "sm" | "md" | "lg" | "xl"the bar'sThe menu's density step. Set a smaller one for a compact menu over a roomy bar
moreModelsPromptInputModelOption[]—Options behind a "More models" submenu, picked into the same value. The trigger shows the chosen one wherever it lives
moreModelsLabelstring"More models"The submenu row's label
childrenReact.ReactNode—Extra content appended inside the menu after the options: a settings switch, a link, a separator
side / alignmenu placement"top" / "start"Where the menu opens
aria-labelstring"Select model"The trigger's name

PromptInputModes

PropTypeDefaultDescription
modesPromptInputMode[]—The mode rows (required). Each: id, icon, label, active / defaultActive, onToggle
iconReact.ReactNodeslidersTrigger icon
aria-labelstring"Prompt modes"Trigger name — constant regardless of state
side / alignmenu placement"top" / "start"Where the menu opens
triggerClassNamestring—Classes on the trigger button

PromptInputSuggestions

PropTypeDefaultDescription
suggestionsPromptInputSuggestion[]—Chips to render (required)
behavior"fill" | "send""fill"Fill the input vs submit on click
onSelect(value: string) => void—Fires when a chip is clicked
chipVariant"solid" | "soft" | "outline" | "ghost""outline"Chip fill style
size"sm" | "md" | "lg"inheritedChip size — inherits the bar's size
rounded"none" | "sm" | "md" | "lg" | "full"inheritedChip rounding — inherits the bar's rounded

PromptInputSkeleton

PropTypeDefaultDescription
size"sm" | "md" | "lg""md"Mirrors the bar size
rounded"none" | "sm" | "md" | "lg" | "full""lg"Mirrors the bar radius
layout"stacked" | "inline""stacked"Mirrors the bar layout
showToolbarbooleantrueRender the toolbar placeholder row
showBannerbooleanfalseReserve the banner row above the textarea, matching banner
showModelbooleanfalseReserve the model select pill, matching models
linesnumber1Text lines in the textarea placeholder. Raise it to depict a drafted message
labelstring—Announces the loading region. Silent when omitted

PromptInputSend

PropTypeDefaultDescription
childrenReact.ReactNode—A visible label beside the icon. Without it Send is the square icon-only button; with it, the labelled CTA that form-shaped composers use
progressnumber—0 to 100. While isGenerating, a ring around Stop fills with it. Report your own measure; the composer never estimates one
sendLabelstring"Send message"The Send button's accessible name
stopLabelstring"Stop generation"The Stop button's accessible name

PromptInputBanner

PropTypeDefaultDescription
iconReact.ReactNode—Leading glyph
intent"none" | "error" | "warning" | "success" | "info""none"Semantic meaning. An error or warning notice is also spoken through the composer's live region when it appears or its text changes, so a limit or quota notice that arrives mid-session is heard
onDismiss() => void—Shows the dismiss control
dismissLabelstring"Dismiss"The dismiss control's accessible name

PromptInputVoiceMode

PropTypeDefaultDescription
isActivebooleanfalseVoice mode is on

PromptInputRecording

PropTypeDefaultDescription
onCancel() => void—Discards the recording. Also bound to Escape anywhere inside the bar
onConfirm() => void—Keeps the recording
elapsednumber—Seconds recorded so far. Omitted, the bar counts from its own mount
amplitudenumber[]—Live input levels, 0 to 1, newest last. Omitted, the bars idle on a fixed shape
formatTime(seconds: number) => string0:07Formats the timer text
confirmIconReact.ReactNodean arrowThe Confirm glyph. The arrow suits a voice note; a check suits dictation
labelstring"Voice recording"The bar's accessible name
cancelLabel · confirmLabelstring"Cancel recording" · "Send recording"The buttons' accessible names. Name what Confirm does with the recording

PromptInputDropOverlay

PropTypeDefaultDescription
isVisibleboolean—Shows the overlay
labelstring"Drop files to attach"Overlay label text

Accessibility

  • One polite live region per composer, mounted empty from first paint (a region added at announcement time is never announced). It speaks: generation started and finished, recording started and stopped, files attached, rejected (with the rule) and removed by name, and entering or leaving the drop zone.
  • Upload progress is spoken at intervals, not continuously — at most once every 10 seconds while a file uploads, and only when the percentage moved; a failure or a finished upload is spoken at once.
  • The attachment rail is a list named "Attachments". Each card reads its file name, its type ("Image", "PDF", "File") and its size or state. An image with alt is announced as an image; without it the thumbnail is decorative, because the name beside it already names the card.
  • Removing or cancelling a card never drops focus to the page. Focus moves to the next card's ×, else the previous card's, else the textarea. After Retry, focus stays on the same card.
  • The recording bar moves focus deliberately. Opening it focuses Confirm (only when focus was already in the composer); closing it returns focus to the textarea. It is a group named "Voice recording", and its level meter is aria-hidden decoration — the timer is the readable signal.
  • The counter follows the tested character-count model — the visible count is aria-hidden and a separate hidden polite region carries the announcement, debounced until typing pauses and silent until the threshold is crossed.
  • Toggles keep a constant name. Voice and voice-mode never rename themselves between states; the state is on aria-pressed, per the APG button pattern. Mode rows are menuitemcheckbox — state on aria-checked, name constant.
  • The model pill announces its value, not just its purpose — "Select model, " followed by the chosen model's own name.
  • Touch targets are ≥44×44 px via an extended hit area centred on each compact control, and adjacent controls are spaced so those areas cannot overlap (12 px below md, giving exactly 44 px of pitch). The rule is simple: the painted icon stays small, the target does not. On a touch device the controls take their phone size at any width, so a tablet has room for the targets too, and the scrolling row of leading controls keeps room for them, so none is cut short.
  • The textarea renders at ≥16 px on phones at every size, so iOS never zooms on focus. It comes from the input-family type ladder, not a local rule.
  • enterKeyHint is stamped from submitOn, so the soft keyboard's Return key is labelled for what it actually does.
  • Focus stays in the textarea while a typeahead is open. The list is a listbox that the textarea points into with aria-activedescendant, and the number of matches is announced through the composer's live region. The textarea keeps its textbox role, which does not allow aria-expanded.

Assistive-technology matrix

ToolStatus
VoiceOver (macOS, Safari)Passed 2026-09-14 — "Ask anything…, text entry area"; Send reads "Send message, dimmed button" while empty and "Send message, button" once there is text
NVDAUntested
JAWSUntested

The mention and command typeahead has not been tested with a screen reader yet.

Migration

Nothing in the TypeScript API was removed or renamed. These behaviours changed:

ChangeBeforeNow
dirAlways stamped ltrUnset unless you pass it — the composer inherits the page direction
maxLengthAdvisory only, despite the docs saying "hard limit"Sending is refused while over the limit (key and button), and the overshoot is announced
disabled + isGeneratingdisabled was ignored while generatingdisabled outranks everything, including Stop
rounded="full"16 px24 px — the panel cap every other panel in the library uses
Desktop textarea size16px (the md: half of the old ladder never applied)14px, as the source always claimed — the composer is ~4px shorter
PromptInputSkeletonAlways announced "Loading"Silent unless you pass label
Model pill name"Select model""Select model, <current model>" — update any name-regex query
Counter markupOne span flipping its own aria-liveVisible count + a separate hidden polite region
A file rejected by accept / maxFiles / maxFileSizeVanished; a callback and a spoken countStays as an error card stating the rule, with a rejection code — onAttachmentsChange receives it, onSend never does
onFilesRejected(files)(files, rejections) — each rejection carries reason and message
Removing an attachmentFocus dropped to <body>Focus moves to the next card, the previous one, or the textarea
Upload progressOnly failure and completion spokenAlso spoken every 10 seconds while the percentage moves
Attachment sizes"20.0 MB""20 MB" — FileUpload's rounding

Agent notes

Every part carries a stable data-slot (prompt-input, prompt-input-textarea, prompt-input-send, prompt-input-attachment, prompt-input-announcer, prompt-input-recording, …), and the root reflects state as data-generating, data-recording, data-disabled, data-dragging, data-over-limit, data-tone, data-size and data-layout. Target those rather than class names or DOM position.

An open typeahead is [data-slot="prompt-input-typeahead"] (the listbox), its rows are [data-slot="prompt-input-typeahead-item"], and the active row carries data-highlighted.

Reading the composer's state from outside React: [data-prompt-input] is the root, its textarea holds the draft, [data-slot="prompt-input-send"] carries data-state="idle" | "generating", and [data-slot="prompt-input-attachment"] rows carry data-status. A card's × is [data-slot="prompt-input-attachment-remove"] (it cancels while uploading when onAttachmentCancel is set) and Retry is [data-slot="prompt-input-attachment-retry"]. A rejection card is an attachment with a rejection field and no file.

Sub-components

ComponentDescription
PromptInputRoot context provider — value, attachments, callbacks, size, layout, direction
PromptInputBannerDismissible notice row at the top of the bar
PromptInputTextareaAuto-growing textarea — expands up to maxRows before scrolling
PromptInputToolbarBottom action row (stacked layout)
PromptInputRowSingle-row wrapper for the inline layout
PromptInputSpacerSpring that pushes the controls after it to the trailing edge: Spacer with grow. Forwards a ref and takes HTML props
PromptInputAttachPaperclip icon button — opens the file picker
PromptInputMenu"+" dropdown of options
PromptInputModelSelectModel-picker pill
PromptInputVoiceMicrophone / dictation button with pulsing recording state
PromptInputVoiceModeSolid voice-mode button
PromptInputRecordingRecording bar — Cancel, level meter, timer, Confirm; replaces the textarea and toolbar while recording
PromptInputModesSliders menu of mode switch rows (Search, Think)
PromptInputCounterCharacter count with limit colouring
PromptInputSendSend / Stop button — swaps based on isGenerating
PromptInputDropOverlayFull-surface drag-to-attach overlay (auto-rendered)
PromptInputAttachmentsAttachment list — thumbnail or file card with progress, and remove, cancel or retry
PromptInputSuggestionsPredefined suggestion chip row
PromptInputSkeletonLoading placeholder mirroring the bar
PromptInputBarAll-in-one compound wrapping every sub-component
ArtifactPanelKeyboardToolbar

On this page

When to useWhen not to useInstallationUsageExamplesDefaultInline layoutToolbar controlsModel selectorModesSelect pillsSuggestionsMentions and commandsAttachmentsUploads, cancel and retryVoice recording barGenerating and stoppingSubmit shortcutCharacter counterBannerLabelled sendOn a coloured surfaceRTLSkeletonCompound APIAPIsPromptInputBarCompound APILayoutModes menuOptions menuModel selectorSuggestionsMentions and commandsAttachmentsVoice & voice modeRecording barSend / Stop swapCharacter counterBannerSkeletonRTLKeyboard behaviourAPI ReferencePromptInputBar — corePromptInputBar — toolbar slotsPromptInputBar — send, suggestions, banner & attachmentsPromptInputAttachmentPromptInputTriggerPromptInputTriggerItemPromptInput (root)PromptInputMenuPromptInputModelSelectPromptInputModesPromptInputSuggestionsPromptInputSkeletonPromptInputSendPromptInputBannerPromptInputVoiceModePromptInputRecordingPromptInputDropOverlayAccessibilityAssistive-technology matrixMigrationAgent notesSub-components