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/uiUsage
import { PromptInputBar } from "@tessinaui/ui";<PromptInputBar
placeholder="Ask anything…"
onSend={(value, attachments) => console.log(value, attachments)}
/>Showcase
APIs
Two APIs are available depending on how much control you need.
| API | When to use |
|---|---|
PromptInputBar | Drop-in single component for the 90% use case |
PromptInput + sub-components | Full 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 />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.
<PromptInputBar
isGenerating={isStreaming}
onSend={(v) => startStream(v)}
onStop={() => abortStream()}
/>Character counter
maxLength enables a live counter that turns warning past 80% and error at
the limit. The count is announced to screen readers only near the limit.
<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.
<PromptInputSkeleton layout="stacked" />
<PromptInputSkeleton layout="inline" rounded="full" />RTL
Pass dir="rtl" to PromptInput or PromptInputBar. The layout, toolbar,
menu, attachments and suggestion rows all mirror automatically.
<PromptInputBar dir="rtl" placeholder="اكتب رسالتك…" onSend={handleSend} />Keyboard behaviour
| Key | Action |
|---|---|
Enter | Submit — calls onSend |
Shift + Enter | Insert newline |
Alt + Enter | Insert newline (alternative) |
| IME composing | Enter submit is suppressed until composition ends |
Accessibility
- Every interactive control exposes a ≥44×44px touch target (WCAG 2.5.5), so the toolbar is comfortable on mobile without enlarging the visual icons.
- The textarea uses a ≥16px font on touch viewports to prevent iOS focus-zoom.
- The send/stop button, voice buttons and menu trigger carry descriptive
aria-labels; the counter announces politely only near the limit.
Sub-components
| Component | Description |
|---|---|
PromptInput | Root context provider — value, attachments, callbacks, size, layout, direction |
PromptInputBanner | Dismissible notice row at the top of the bar |
PromptInputTextarea | Auto-growing textarea — expands up to maxRows before scrolling |
PromptInputToolbar | Bottom action row (stacked layout) |
PromptInputRow | Single-row wrapper for the inline layout |
PromptInputSpacer | flex-1 spacer to push items to either end |
PromptInputAttach | Paperclip icon button — opens the file picker |
PromptInputMenu | "+" dropdown of options |
PromptInputModelSelect | Model-picker pill |
PromptInputVoice | Microphone / dictation button with pulsing recording state |
PromptInputVoiceMode | Solid voice-mode button |
PromptInputCounter | Character count with limit colouring |
PromptInputSend | Send / Stop button — swaps based on isGenerating |
PromptInputDropOverlay | Full-surface drag-to-attach overlay (auto-rendered) |
PromptInputAttachments | Inline attachment previews with remove buttons |
PromptInputSuggestions | Predefined suggestion chip row |
PromptInputSkeleton | Loading placeholder mirroring the bar |
PromptInputBar | All-in-one compound wrapping every sub-component |
API Reference
PromptInputBar — core
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Controlled value |
defaultValue | string | "" | 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 |
isGenerating | boolean | false | Swaps Send for Stop |
size | "sm" | "md" | "lg" | "md" | Textarea and button sizing |
rounded | "sm" | "md" | "lg" | "xl" | "full" | "xl" | Container border-radius |
layout | "stacked" | "inline" | "stacked" | Toolbar-below vs single-row layout |
disabled | boolean | false | Disables all interactions |
dir | "ltr" | "rtl" | "ltr" | Text direction |
placeholder | string | "Message…" | Textarea placeholder |
maxRows | number | 8 | Lines before the textarea scrolls |
maxLength | number | — | Hard character limit; enables the counter |
showCounter | boolean | false | Force-show the counter without maxLength |
PromptInputBar — toolbar slots
| Prop | Type | Default | Description |
|---|---|---|---|
showMenu | boolean | false | Show the "+" options menu |
menuItems | PromptInputMenuItem[] | default set | Custom menu items |
showAttach | boolean | false | Show the paperclip attach button |
onAttach | () => void | — | Overrides the attach button click |
showVoice | boolean | false | Show the microphone button |
onVoice | () => void | — | Called when the mic is clicked |
isRecording | boolean | false | Pulsing recording state on the mic |
showVoiceMode | boolean | false | Show the solid voice-mode button |
onVoiceMode | () => void | — | Called when voice mode is clicked |
isVoiceModeActive | boolean | false | Active state on the voice-mode button |
models | PromptInputModelOption[] | — | Renders the model-selector pill |
model / defaultModel | string | — | Controlled / uncontrolled model value |
onModelChange | (value: string) => void | — | Fires when the model changes |
PromptInputBar — suggestions, banner & attachments
| Prop | Type | Default | Description |
|---|---|---|---|
suggestions | PromptInputSuggestion[] | — | 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 |
banner | React.ReactNode | — | Dismissible banner content |
bannerIcon | React.ReactNode | — | Leading icon for the banner |
onBannerDismiss | () => void | — | Called when the banner is dismissed |
accept | string | — | Accepted file types; enables attachments |
maxFiles | number | — | Maximum attachment count |
maxFileSize | number | — | Maximum size per file, in bytes |
attachments / defaultAttachments | PromptInputAttachment[] | — | Controlled / uncontrolled attachment list |
onAttachmentsChange | (attachments: PromptInputAttachment[]) => void | — | Fires when attachments change |
onFilesRejected | (files: File[]) => void | — | Files rejected by the accept / size / count limits |
attachable | boolean | auto | Force-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
| Prop | Type | Default | Description |
|---|---|---|---|
items | PromptInputMenuItem[] | default set | Menu items |
children | React.ReactNode | — | Custom menu content — overrides items |
icon | React.ReactNode | <Plus /> | Trigger icon |
side | "top" | "bottom" | "top" | Menu open direction |
align | "start" | "center" | "end" | "start" | Menu alignment |
PromptInputModelSelect
| Prop | Type | Default | Description |
|---|---|---|---|
models | PromptInputModelOption[] | — | Selectable models (required) |
value / defaultValue | string | — | Controlled / uncontrolled selection |
onValueChange | (value: string) => void | — | Fires on selection |
PromptInputSuggestions
| Prop | Type | Default | Description |
|---|---|---|---|
suggestions | PromptInputSuggestion[] | — | 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" | inherited | Chip size — inherits the bar's size |
rounded | "sm" | "md" | "lg" | "xl" | "full" | inherited | Chip rounding — inherits the bar's rounded |
PromptInputSkeleton
| Prop | Type | Default | Description |
|---|---|---|---|
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 |
showToolbar | boolean | true | Render the toolbar placeholder row |
PromptInputDropOverlay
| Prop | Type | Default | Description |
|---|---|---|---|
isVisible | boolean | — | Shows the overlay |
label | string | "Drop files to attach" | Overlay label text |