DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

PromptInput

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

Playground

Installation

pnpm add @tessinaui/ui

Usage

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

When to use

PromptInput is the composer for a conversational surface: an auto-growing message field with the controls that send, attach to, and configure one turn.

Reach for something else when:

You wantUse
A plain multi-line form fieldTextarea
A search field with a submit affordanceSearch
A command palette over actionsCommand
The message bubbles themselvesChatBubble
A standalone drop zone with a file listFileUpload

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 stacked bar: auto-growing textarea with a toolbar underneath.

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 menu

The modes a prompt runs with, as switch rows behind one sliders trigger — the shape the flagship assistants ship (Claude's sliders menu, ChatGPT's Tools menu). The menu stays open while several modes flip, and the trigger tints while any mode is on.

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 ("Sonnet 4.6 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

Attachments

Drag a file onto the bar, paste an image, or use the paperclip. Report transfer state per file with status, progress and errorMessage.

Generating and stopping

Submit shortcut

Character counter

Banner

Labelled send

On a coloured surface

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.

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

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 ChatGPT-style pill. In the compound API, wrap the row in PromptInputRow.

<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 (Perplexity's "Search ▾"), use PromptInputModelSelect instead — it is the generic single-select pill.

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

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

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.

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

Attachments

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

<PromptInputBar
  showMenu
  accept="image/*,.pdf,.txt"
  maxFiles={4}
  maxFileSize={5 * 1024 * 1024}
  onSend={(text, files) => send(text, files)}
  onFilesRejected={(rejected) => toast(`${rejected.length} file(s) rejected`)}
/>

Attachment state can also be controlled with attachments / onAttachmentsChange.

Voice & voice mode

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

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

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.

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

<PromptInputBar maxLength={500} showCounter />

Banner

banner renders a dismissible notice at the top of the bar — handy for usage notes or model warnings.

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

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

<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
Tab / Shift + TabMove through the toolbar controls — each is its own stop
Space / Enter on a toggleFlip a mode, voice or voice-mode button

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:

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

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

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, files attached, rejected and removed by name, and entering or leaving the drop zone.
  • The counter follows the GOV.UK 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, Sonnet 4.6".
  • 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). This is the M3 model: 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.

Assistive-technology matrix

ToolStatus
VoiceOver (macOS, Safari)Verified
NVDAUntested
JAWSUntested

Do and don't

Do

  • Report 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.
  • Give 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 composer's own controls to what configures this turn.

Don't

  • Don't put a role="toolbar" wrapper around the controls — see above.
  • Don't rename a toggle when it flips ("Voice input" → "Stop voice input"); that breaks the toggle contract screen readers rely on.
  • Don't rely on the composer to upload, transcribe or call a model. Wire those yourself and report the result back through props.
  • 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).

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

Agent notes

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

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.

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
PromptInputSpacerflex-1 spacer to push items to either end
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
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)
PromptInputAttachmentsInline attachment previews with remove buttons
PromptInputSuggestionsPredefined suggestion chip row
PromptInputSkeletonLoading placeholder mirroring the bar
PromptInputBarAll-in-one compound wrapping every sub-component

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""default"Set on a coloured or dark plate — the well derives its ink from currentColor
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

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
isRecordingbooleanfalsePulsing recording state on the mic
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 also make the row a polite live region

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
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"
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[]) => void—Files rejected by the accept / size / count limits
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

PromptInputModelSelect

PropTypeDefaultDescription
modelsPromptInputModelOption[]—Selectable models (required)
value / defaultValuestring—Controlled / uncontrolled selection
onValueChange(value: string) => void—Fires on selection

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

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"sm" | "md" | "lg" | "xl" | "full"inheritedChip rounding — inherits the bar's rounded

PromptInputSkeleton

PropTypeDefaultDescription
size"sm" | "md" | "lg""md"Mirrors the bar size
rounded"sm" | "md" | "lg" | "xl" | "full""xl"Mirrors the bar radius
layout"stacked" | "inline""stacked"Mirrors the bar layout
showToolbarbooleantrueRender the toolbar placeholder row

PromptInputDropOverlay

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

On this page

PlaygroundInstallationUsageWhen to useExamplesDefaultInline layoutToolbar controlsModel selectorModes menuSelect pillsSuggestionsAttachmentsGenerating and stoppingSubmit shortcutCharacter counterBannerLabelled sendOn a coloured surfaceRTLSkeletonCompound APIAPIsPromptInputBarCompound APILayoutModes menuOptions menuModel selectorSuggestionsAttachmentsVoice & voice modeSend / Stop swapCharacter counterBannerSkeletonRTLKeyboard behaviourAccessibilityAssistive-technology matrixDo and don'tMigrationAgent notesSub-componentsAPI ReferencePromptInputBar — corePromptInputBar — toolbar slotsPromptInputBar — send, suggestions, banner & attachmentsPromptInputAttachmentPromptInput (root)PromptInputMenuPromptInputModelSelectPromptInputModesPromptInputSuggestionsPromptInputSkeletonPromptInputDropOverlay