PromptInput
The cornerstone compose surface for any AI chat. A form-rooted state machine with auto-resizing textarea, attachment chips, action menu, speech input, model selector and a status-driven submit button.
Overview
PromptInput is the largest primitive in @gremorie/rx-ai. It is a <form>-rooted state machine that composes an InputGroup from @gremorie/rx-forms (textarea + addons rendered as one cohesive surface) with the full set of chat-input affordances: file attachments with hover-card previews, an action menu, a speech-to-text button, a model selector, in-input action buttons and a status-driven submit button whose icon flips between arrow / spinner / stop / X depending on chat state.
State is managed in three layers:
- Self-managed (default). Everything lives inside
PromptInput. PassonSubmitand you are done. - Provider-lifted via
PromptInputProvider. ExposeusePromptInputController()to read or mutate the text and attachments from anywhere in the tree. - Controlled textarea. The provider's
textInput.value/textInput.setInputdrivePromptInputTextareaautomatically when the provider is present.
The component also carries an enforced keyboard contract: Enter submits, Shift+Enter newlines, Backspace on an empty textarea removes the last attachment, paste-with-files attaches automatically, IME composition events are honored. Submit is wired through form.requestSubmit() so HTML form validation still applies.
Preview
'use client';import { PromptInput, PromptInputActionAddAttachments, PromptInputActionMenu, PromptInputActionMenuContent, PromptInputActionMenuItem, PromptInputActionMenuTrigger, PromptInputAttachment, PromptInputAttachments, PromptInputBody, PromptInputButton, PromptInputFooter, PromptInputSelect, PromptInputSelectContent, PromptInputSelectItem, PromptInputSelectTrigger, PromptInputSelectValue, PromptInputSubmit, PromptInputTextarea, PromptInputTools,} from '@gremorie/rx-ai';import { GlobeIcon, MicIcon } from 'lucide-react';const MODELS: { id: string; label: string; disabled?: boolean }[] = [ { id: 'claude-opus-4-7', label: 'Claude Opus 4.7' }, { id: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6' }, { id: 'claude-haiku-4-5', label: 'Claude Haiku 4.5' }, { id: 'gpt-5', label: 'GPT-5', disabled: true },];export function PromptInputPreview() { return ( <PromptInput className="mx-auto max-w-xl" globalDrop multiple onSubmit={() => { // demo: wire onSubmit to the AI SDK to make this real }} > <PromptInputBody> <PromptInputAttachments> {(attachment) => <PromptInputAttachment data={attachment} />} </PromptInputAttachments> <PromptInputTextarea placeholder="Ask anything..." /> </PromptInputBody> <PromptInputFooter> <PromptInputTools> <PromptInputActionMenu> <PromptInputActionMenuTrigger aria-label="More actions" /> <PromptInputActionMenuContent> <PromptInputActionAddAttachments /> <PromptInputActionMenuItem> <MicIcon className="mr-2 size-4" /> Record voice </PromptInputActionMenuItem> </PromptInputActionMenuContent> </PromptInputActionMenu> <PromptInputButton aria-label="Search the web"> <GlobeIcon className="size-4" /> <span>Search</span> </PromptInputButton> <PromptInputSelect defaultValue="claude-sonnet-4-6"> <PromptInputSelectTrigger> <PromptInputSelectValue placeholder="Select model" /> </PromptInputSelectTrigger> <PromptInputSelectContent> {MODELS.map((model) => ( <PromptInputSelectItem disabled={model.disabled} key={model.id} value={model.id} > {model.label} </PromptInputSelectItem> ))} </PromptInputSelectContent> </PromptInputSelect> </PromptInputTools> <PromptInputSubmit status="ready" /> </PromptInputFooter> </PromptInput> );}Anatomy
PromptInput
├─ PromptInputHeader
│ ├─ PromptInputMentions @ add-context picker
│ └─ Context token-usage meter
├─ PromptInputBody
│ ├─ PromptInputAttachments optional
│ └─ PromptInputTextarea
└─ PromptInputFooter
├─ PromptInputTools left · mode + model
│ ├─ PromptInputSelect mode
│ └─ PromptInputSelect model
└─ PromptInputTools right
├─ PromptInputButton web search
├─ PromptInputAttachButton attach
├─ PromptInputSpeechButton voice
└─ PromptInputSubmit sendInstallation
bash npx gremorie@latest add rx-prompt-input bash pnpm dlx gremorie@latest add rx-prompt-input
bash yarn dlx gremorie@latest add rx-prompt-input
bash bunx --bun gremorie@latest add rx-prompt-input
Brings in rx-forms (InputGroup family + Button + Select), rx-overlays (DropdownMenu + HoverCard + Tooltip), rx-display (Command + Dialog) and rx-core (the cn helper) as registry dependencies.
Usage
Minimal
"use client";
import { useState } from "react";
import {
type ChatStatus,
PromptInput,
PromptInputBody,
PromptInputFooter,
PromptInputSubmit,
PromptInputTextarea,
} from "@gremorie/rx-ai";
export function ChatBox() {
const [status, setStatus] = useState<ChatStatus>("ready");
return (
<PromptInput onSubmit={(message) => send(message.text, message.files)}>
<PromptInputBody>
<PromptInputTextarea placeholder="Ask anything" />
</PromptInputBody>
<PromptInputFooter>
<span />
<PromptInputSubmit status={status} />
</PromptInputFooter>
</PromptInput>
);
}
import {
PromptInput,
PromptInputSubmit,
PromptInputTextarea,
PromptInputToolbar,
} from "@gremorie/ng-ai";
@Component({
selector: "app-chat-box",
standalone: true,
imports: [PromptInput, PromptInputTextarea, PromptInputToolbar, PromptInputSubmit],
template: `
<prompt-input (submitted)="send($event)">
<prompt-input-textarea placeholder="Ask anything" />
<prompt-input-toolbar>
<prompt-input-submit [status]="status()" />
</prompt-input-toolbar>
</prompt-input>
`,
})
export class ChatBoxComponent {
status = signal<ChatStatus>("ready");
}Full-featured
'use client';
import { useRef, useState } from 'react';
import {
type ChatStatus,
PromptInput,
PromptInputActionAddAttachments,
PromptInputActionMenu,
PromptInputActionMenuContent,
PromptInputActionMenuTrigger,
PromptInputAttachment,
PromptInputAttachments,
PromptInputBody,
PromptInputButton,
PromptInputFooter,
PromptInputSelect,
PromptInputSelectContent,
PromptInputSelectItem,
PromptInputSelectTrigger,
PromptInputSelectValue,
PromptInputSpeechButton,
PromptInputSubmit,
PromptInputTextarea,
PromptInputTools,
} from '@gremorie/rx-ai';
import { GlobeIcon } from 'lucide-react';
const models = [
{ id: 'gpt-4o', label: 'GPT-4o' },
{ id: 'claude-4.5', label: 'Claude 4.5' },
];
export function ChatBox() {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [status, setStatus] = useState<ChatStatus>('ready');
const [model, setModel] = useState('gpt-4o');
return (
<PromptInput
accept="image/*"
multiple
maxFileSize={5_000_000}
onSubmit={(message) => send(message.text, message.files)}
>
<PromptInputBody>
<PromptInputAttachments>
{(file) => <PromptInputAttachment data={file} />}
</PromptInputAttachments>
<PromptInputTextarea ref={textareaRef} placeholder="Ask anything" />
</PromptInputBody>
<PromptInputFooter>
<PromptInputTools>
<PromptInputActionMenu>
<PromptInputActionMenuTrigger aria-label="Add attachment" />
<PromptInputActionMenuContent>
<PromptInputActionAddAttachments />
</PromptInputActionMenuContent>
</PromptInputActionMenu>
<PromptInputSpeechButton textareaRef={textareaRef} />
<PromptInputButton aria-label="Search the web">
<GlobeIcon />
Web
</PromptInputButton>
<PromptInputSelect value={model} onValueChange={setModel}>
<PromptInputSelectTrigger>
<PromptInputSelectValue placeholder="Model" />
</PromptInputSelectTrigger>
<PromptInputSelectContent>
{models.map((m) => (
<PromptInputSelectItem key={m.id} value={m.id}>
{m.label}
</PromptInputSelectItem>
))}
</PromptInputSelectContent>
</PromptInputSelect>
</PromptInputTools>
<PromptInputSubmit status={status} />
</PromptInputFooter>
</PromptInput>
);
}Recipes
PromptInput is customized by composition, not configuration. There is no
showAttachments or showVoice prop, by design (the same contract as Vercel
AI Elements): every part is a component you include in the tree or leave out.
To remove the context section, the voice button or the attachments flow, you
delete the part from your code instead of toggling a flag. The recipes below
are complete, copyable starting points ordered from the smallest possible
composer to the full B2B surface.
Minimal
The smallest valid composition: a body with the textarea and a footer with the
submit button. The empty <span /> keeps the footer's justify-between
layout pushing submit to the right. Everything else is opt-in.
'use client';import { PromptInput, PromptInputBody, PromptInputFooter, PromptInputSubmit, PromptInputTextarea,} from '@gremorie/rx-ai';export function PromptInputMinimalPreview() { return ( <PromptInput className="mx-auto max-w-xl" onSubmit={() => { // demo: wire onSubmit to the AI SDK to make this real }} > <PromptInputBody> <PromptInputTextarea placeholder="Ask anything..." /> </PromptInputBody> <PromptInputFooter> <span /> <PromptInputSubmit status="ready" /> </PromptInputFooter> </PromptInput> );}With attachments
Adds the attach affordance and the chips row. PromptInputAttachButton opens
the file picker, and PromptInputAttachments renders one chip per attached
file (paste and drag-drop funnel into the same list). The standalone
Attachments row seeds the preview with sample files so you can see the chip
treatment per media type.
'use client';import { type AttachmentFile, Attachments, PromptInput, PromptInputAttachButton, PromptInputAttachment, PromptInputAttachments, PromptInputBody, PromptInputFooter, PromptInputSubmit, PromptInputTextarea, PromptInputTools,} from '@gremorie/rx-ai';// Pre-seeded demo chips so the surface is not empty. Files the user// actually attaches flow through PromptInputAttachments below.const SAMPLE_FILES: AttachmentFile[] = [ { id: 'chart', type: 'file', url: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=400&h=400&fit=crop', mediaType: 'image/jpeg', filename: 'q3-revenue-chart.png', }, { id: 'pdf', type: 'file', url: '', mediaType: 'application/pdf', filename: 'q3-report.pdf', }, { id: 'call', type: 'file', url: '', mediaType: 'audio/mpeg', filename: 'earnings-call.mp3', },];export function PromptInputAttachmentsPreview() { return ( <PromptInput className="mx-auto max-w-xl" globalDrop multiple onSubmit={() => { // demo: wire onSubmit to the AI SDK to make this real }} > <PromptInputBody> <Attachments className="w-full px-3" variant="inline"> {SAMPLE_FILES.map((file) => ( <PromptInputAttachment data={file} key={file.id} /> ))} </Attachments> <PromptInputAttachments> {(attachment) => <PromptInputAttachment data={attachment} />} </PromptInputAttachments> <PromptInputTextarea placeholder="Ask about these files..." /> </PromptInputBody> <PromptInputFooter> <PromptInputTools> <PromptInputAttachButton tooltip="Attach files" /> </PromptInputTools> <PromptInputSubmit status="ready" /> </PromptInputFooter> </PromptInput> );}Full B2B
The Gremorie-standard business composition, the same surface the Assistant
block ships: an @ add-context command palette plus a token-usage meter in
the header, Mode and Model selects with provider icons in the footer, and a
web search / attach / voice icon group next to the submit button. Every
action carries a tooltip.
'use client';import { Context, ContextCacheUsage, ContextContent, ContextContentBody, ContextContentFooter, ContextContentHeader, ContextInputUsage, ContextOutputUsage, ContextReasoningUsage, ContextTrigger, PromptInput, PromptInputAttachButton, PromptInputBody, PromptInputButton, PromptInputFooter, PromptInputHeader, PromptInputMentions, type PromptInputMentionsItem, PromptInputSelect, PromptInputSelectContent, PromptInputSelectItem, PromptInputSelectTrigger, PromptInputSelectValue, PromptInputSpeechButton, PromptInputSubmit, PromptInputTextarea, PromptInputTools,} from '@gremorie/rx-ai';import { ClaudeIcon, GeminiIcon, OpenAiIcon } from '@gremorie/rx-icons';import { Tooltip, TooltipContent, TooltipTrigger } from '@gremorie/rx-overlays';import { ArrowUpIcon, BookOpenIcon, ChartColumnBigIcon, DatabaseIcon, FileSpreadsheetIcon, GitBranchIcon, GlobeIcon, LayersIcon, ListChecksIcon, MessageCircleQuestionIcon, NotebookTextIcon, TelescopeIcon,} from 'lucide-react';import type { LanguageModelUsage } from 'ai';const MODES = [ { id: 'ask', label: 'Ask', icon: <MessageCircleQuestionIcon className="size-4" />, }, { id: 'analyze', label: 'Analyze', icon: <ChartColumnBigIcon className="size-4" />, }, { id: 'research', label: 'Research', icon: <TelescopeIcon className="size-4" />, }, { id: 'plan', label: 'Plan', icon: <ListChecksIcon className="size-4" /> },];const MODELS = [ { id: 'claude-opus-4-8', label: 'Claude Opus 4.8', icon: <ClaudeIcon className="size-4" />, }, { id: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6', icon: <ClaudeIcon className="size-4" />, }, { id: 'gpt-5', label: 'GPT-5', icon: <OpenAiIcon className="size-4" /> }, { id: 'gemini-2-5-pro', label: 'Gemini 2.5 Pro', icon: <GeminiIcon className="size-4" />, },];// Example context catalogue for the "@ Add context" command palette.const CONTEXT_ITEMS: PromptInputMentionsItem[] = [ { id: 'q3-revenue', label: 'Q3 revenue.csv', group: 'Recent', icon: <FileSpreadsheetIcon />, }, { id: 'brand', label: 'Brand guidelines', group: 'Recent', icon: <BookOpenIcon />, }, { id: 'onboarding-prd', label: 'Onboarding PRD', group: 'Recent', icon: <NotebookTextIcon />, }, { id: 'notion-roadmap', label: 'Notion: Roadmap', group: 'Workspace', icon: <DatabaseIcon />, }, { id: 'linear-sprint', label: 'Linear: Sprint 24', group: 'Workspace', icon: <LayersIcon />, }, { id: 'github-app', label: 'GitHub: gremorie/app', group: 'Workspace', icon: <GitBranchIcon />, },];// Per-category token usage powering the context hovercard breakdown + cost.const USAGE: LanguageModelUsage = { inputTokens: 52_600, outputTokens: 6_400, reasoningTokens: 2_200, cachedInputTokens: 14_000, totalTokens: 61_200, inputTokenDetails: { noCacheTokens: 38_600, cacheReadTokens: 14_000, cacheWriteTokens: 0, }, outputTokenDetails: { textTokens: 6_400, reasoningTokens: 2_200, },};// Outlined trigger to match the Button "outline" variant. The default// PromptInputSelectTrigger is borderless/ghost, so re-add the border and keep// the fill transparent so the control sits flush on the card surface.const OUTLINE_TRIGGER = 'border border-input border-solid bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground aria-expanded:bg-accent';export function PromptInputB2BPreview() { return ( <PromptInput className="mx-auto max-w-2xl" globalDrop multiple onSubmit={() => { // demo: wire onSubmit to the AI SDK to make this real }} > <PromptInputHeader> <PromptInputMentions items={CONTEXT_ITEMS} /> <Context maxTokens={200_000} modelId="anthropic:claude-3-5-sonnet" usage={USAGE} usedTokens={62_600} > <ContextTrigger className="ml-auto" /> <ContextContent> <ContextContentHeader /> <ContextContentBody> <div className="space-y-1"> <ContextInputUsage /> <ContextOutputUsage /> <ContextReasoningUsage /> <ContextCacheUsage /> </div> </ContextContentBody> <ContextContentFooter /> </ContextContent> </Context> </PromptInputHeader> <PromptInputBody> <PromptInputTextarea placeholder="What would you like to know?" /> </PromptInputBody> <PromptInputFooter> <PromptInputTools className="gap-2"> <PromptInputSelect defaultValue="research"> <Tooltip> <TooltipTrigger asChild> <PromptInputSelectTrigger aria-label="Select mode" className={OUTLINE_TRIGGER} size="sm" > <PromptInputSelectValue placeholder="Mode" /> </PromptInputSelectTrigger> </TooltipTrigger> <TooltipContent>Response mode</TooltipContent> </Tooltip> <PromptInputSelectContent> {MODES.map((mode) => ( <PromptInputSelectItem key={mode.id} value={mode.id}> {mode.icon} {mode.label} </PromptInputSelectItem> ))} </PromptInputSelectContent> </PromptInputSelect> <PromptInputSelect defaultValue="claude-sonnet-4-6"> <Tooltip> <TooltipTrigger asChild> <PromptInputSelectTrigger aria-label="Select model" className={OUTLINE_TRIGGER} size="sm" > <PromptInputSelectValue placeholder="Model" /> </PromptInputSelectTrigger> </TooltipTrigger> <TooltipContent>Model</TooltipContent> </Tooltip> <PromptInputSelectContent> {MODELS.map((model) => ( <PromptInputSelectItem key={model.id} value={model.id}> {model.icon} {model.label} </PromptInputSelectItem> ))} </PromptInputSelectContent> </PromptInputSelect> </PromptInputTools> <PromptInputTools className="gap-2"> <PromptInputTools> <PromptInputButton aria-label="Search the web" tooltip="Search the web" > <GlobeIcon className="size-4" /> </PromptInputButton> <PromptInputAttachButton tooltip="Attach files" /> <PromptInputSpeechButton tooltip="Voice input" /> </PromptInputTools> <PromptInputSubmit status="ready" tooltip="Send"> <ArrowUpIcon className="size-4" /> </PromptInputSubmit> </PromptInputTools> </PromptInputFooter> </PromptInput> );}API
Types
| Type | Shape | Description |
|---|---|---|
ChatStatus | "ready" | "submitted" | "streaming" | "error" | The lifecycle states PromptInputSubmit reacts to. |
FileUIPart | { type: "file"; url: string; mediaType: string; filename?: string } | The Vercel AI SDK's file part shape. Exported here for convenience. |
AttachmentFile | FileUIPart & { id: string } | Internal representation that adds a stable ID. Used by the attachments context. |
PromptInputMessage | { text: string; files: FileUIPart[] } | What onSubmit receives. |
<PromptInput>
The root form. Manages attachments, file drag-drop and submit lifecycle. Use a PromptInputProvider parent to lift state outside.
| Prop | Type | Default | Description |
|---|---|---|---|
onSubmit | (message: PromptInputMessage, event: FormEvent) => void | Promise<void> | - | Required. Fires on form submit (Enter key, Submit click, or requestSubmit). |
accept | string | - | Comma-separated MIME pattern ("image/*,application/pdf"). Validates pasted and dropped files. |
multiple | boolean | - | Allow more than one file per pick. |
globalDrop | boolean | false | When true, accepts drops anywhere on the document. Default scope is the form element. |
syncHiddenInput | boolean | false | Clear the hidden file input when the attachments list goes to 0. Useful for re-picking the same filename. |
maxFiles | number | - | Cap on total attachments. Extra files trigger onError({ code: "max_files" }). |
maxFileSize | number | - | Per-file byte cap. Files over the limit trigger onError({ code: "max_file_size" }). |
onError | (err: { code: "max_files" | "max_file_size" | "accept"; message: string }) => void | - | Fires when validation rejects files. |
className | string | - | Extra classes on the underlying InputGroup. |
children | ReactNode | - | Typically PromptInputBody and its descendants. |
<PromptInputProvider>
Optional provider that lifts text and attachment state above PromptInput. Wrap your tree once near the root, then any descendant can use usePromptInputController() or useProviderAttachments().
| Prop | Type | Default | Description |
|---|---|---|---|
initialInput | string | "" | Seed value for the controlled textarea. |
children | ReactNode | - | The tree that should observe the lifted state. |
<PromptInputBody> / <PromptInputHeader> / <PromptInputFooter> / <PromptInputTools>
Layout primitives.
PromptInputBodywraps children indisplay: contentsso they flow directly into theInputGroup.PromptInputHeaderis anInputGroupAddonpinned toalign="block-end"withorder-first flex-wrap gap-1. Use for header-style toolbars (above the textarea).PromptInputFooteris anInputGroupAddonpinned toalign="block-end"withjustify-between gap-1. Default home for tools + submit.PromptInputToolsis aflex items-center gap-1row meant to live inside the footer.
All extend their underlying DOM element props.
<PromptInputTextarea>
The composer. Auto-resizes between min-h-16 and max-h-48. Default name="message" so it serializes as the form's text field.
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder | string | "What would you like to know?" | Placeholder text. |
value / onChange | controlled props | - | When provided, the textarea is fully controlled. Otherwise the provider's text input drives it (if any). |
Built-in keyboard contract:
| Key | Behavior |
|---|---|
Enter | requestSubmit() (skipped if Submit is disabled) |
Shift+Enter | Newline |
Backspace on empty textarea | Removes the most recent attachment |
| IME composition | Enter is ignored while composing |
| Paste with files | Attaches files automatically |
<PromptInputAttachments> / <PromptInputAttachment>
| Component | Purpose |
|---|---|
PromptInputAttachments | Container that renders one child per attachment via a function child. Renders nothing when the list is empty. |
PromptInputAttachment | A single chip with a hover-card preview (image thumbnail or paperclip icon) and a remove button. |
PromptInputAttachments:
| Prop | Type | Default | Description |
|---|---|---|---|
children | (attachment: AttachmentFile) => ReactNode | - | Required. Render function. |
className | string | - | Extra classes on the wrapper. |
PromptInputAttachment:
| Prop | Type | Default | Description |
|---|---|---|---|
data | AttachmentFile | - | Required. The attachment to render. |
className | string | - | Extra classes on the chip. |
<PromptInputButton> / <PromptInputSubmit>
PromptInputButton wraps InputGroupButton. Picks size="icon-sm" for single-child icons and size="sm" for icon+label, unless you set size explicitly. Default variant="ghost".
PromptInputSubmit is a status-driven InputGroupButton whose icon depends on status:
status | Icon | Behavior |
|---|---|---|
"ready" | CornerDownLeftIcon | Submit when clicked. Disabled when the textarea is empty. |
"submitted" | Loader2Icon (spinning) | Disabled. |
"streaming" | SquareIcon | Click to stop generation (host wires up the handler). |
"error" | XIcon | Click to retry (host wires up the handler). |
undefined | CornerDownLeftIcon | Same as "ready". |
Carries aria-label="Submit".
Action menu family
PromptInputActionMenu, PromptInputActionMenuTrigger, PromptInputActionMenuContent and PromptInputActionMenuItem are thin wrappers around DropdownMenu, DropdownMenuTrigger, DropdownMenuContent and DropdownMenuItem. The trigger defaults to a PromptInputButton with a PlusIcon.
PromptInputActionAddAttachments is a pre-wired DropdownMenuItem that calls attachments.openFileDialog() on select. Default label: "Add photos or files". Accepts a label prop to override.
<PromptInputSpeechButton>
Web Speech API toggle.
| Prop | Type | Default | Description |
|---|---|---|---|
textareaRef | RefObject<HTMLTextAreaElement | null> | - | When provided, transcribed text is appended to the textarea value. |
onTranscriptionChange | (text: string) => void | - | Fires whenever a final transcript is appended. |
lang | string | "en-US" | BCP-47 language tag passed to SpeechRecognition. |
Carries aria-label="Toggle voice input". Disabled when neither SpeechRecognition nor webkitSpeechRecognition is available. Animates a pulsing accent surface while listening.
<PromptInputSelect> family
Wrappers around Select from @gremorie/rx-forms (PromptInputSelect, PromptInputSelectTrigger, PromptInputSelectContent, PromptInputSelectItem, PromptInputSelectValue). The trigger is restyled to look inline inside the input row (borderless, transparent background, muted foreground).
<PromptInputHoverCard> family
PromptInputHoverCard, PromptInputHoverCardTrigger, PromptInputHoverCardContent. HoverCard from @gremorie/rx-overlays with openDelay and closeDelay defaulted to 0 so previews feel instant. Content defaults to align="start". Used internally by PromptInputAttachment.
<PromptInputCommand> family
PromptInputCommand, PromptInputCommandInput, PromptInputCommandList, PromptInputCommandEmpty, PromptInputCommandGroup, PromptInputCommandItem, PromptInputCommandSeparator. Thin wrappers around the Command palette primitives.
PromptInputCommandDialog is the Cmd+K floating palette - same Command palette inside a Dialog for global launcher use.
<PromptInputTabs*> family
PromptInputTabsList, PromptInputTab, PromptInputTabLabel, PromptInputTabBody, PromptInputTabItem. Lightweight non-stateful tabbed panels for use inside menus and dropdowns. They are styled containers - bring your own selection state.
Hooks
| Hook | Purpose |
|---|---|
usePromptInputController() | Read or mutate the text input value and attachments. Throws if no PromptInputProvider parent. |
useProviderAttachments() | Read attachment state lifted to the provider. Throws if no PromptInputProvider parent. |
usePromptInputAttachments() | Read attachment state from the closest PromptInput. Used inside descendants like PromptInputTextarea and PromptInputAttachment. |
Composition
PromptInput (root <form>, state machine + drag-drop + InputGroup wrapper)
PromptInputBody (display: contents, wraps only the attachments + textarea)
PromptInputAttachments (render-prop list of chips, hidden when empty)
PromptInputAttachment (chip + HoverCard preview + remove button)
PromptInputTextarea (auto-resize, Enter submits, Shift+Enter newline)
PromptInputHeader (optional, sibling of Body, renders above the textarea)
PromptInputFooter (default home for tools + submit, sibling of Body)
PromptInputTools (left, action buttons)
PromptInputActionMenu (dropdown trigger + content + items)
PromptInputActionAddAttachments (pre-wired item)
PromptInputSpeechButton (Web Speech toggle)
PromptInputButton (custom actions - web search, etc.)
PromptInputSelect (model picker)
PromptInputSubmit (right, status-driven icon)[!important]
PromptInputHeaderandPromptInputFootermust be siblings ofPromptInputBody, not nested inside it.PromptInputBodyisdisplay: contents, and the underlyingInputGrouponly switches to its stacked column layout when adata-align="block-end"addon is its direct child. A footer nested inside the contents Body is not a direct child, so the group stays a single row and the textarea collapses into a thin bar.
How the parts plug together
- InputGroup wrapping: The React root wraps
<form>content in anInputGroupfrom@gremorie/rx-forms. That gives you a single rounded surface with shared focus, invalid and disabled states across the textarea and every addon. - Block-aligned addons:
PromptInputHeaderandPromptInputFooterare bothInputGroupAddon align="block-end". They render below the textarea visually but participate in the group's focus ring. - Sizing: Every toolbar button picks
xs,sm,icon-xsoricon-smto match the group height automatically.PromptInputButtonpicksicon-smwhen it has a single child andsmwhen it has icon+label, unless you override. - Attachment state context:
PromptInput(orPromptInputProviderwhen present) provides the attachments context thatPromptInputAttachments,PromptInputAttachmentandPromptInputActionAddAttachmentsall read. Adding via the action menu, paste, drag-drop orattachments.add(files)all funnel into the same list.
Variations
Lifted state via Provider
Use PromptInputProvider to share the textarea value and attachments with siblings (e.g. a suggestions panel that wants to write into the input).
import { PromptInputProvider, usePromptInputController } from '@gremorie/rx-ai';
function SuggestionRow() {
const { textInput } = usePromptInputController();
return (
<Button onClick={() => textInput.setInput('Summarize this thread')}>
Insert suggestion
</Button>
);
}
export function ComposerExample() {
return (
<PromptInputProvider initialInput="">
<SuggestionRow />
<PromptInput onSubmit={handle}>
<PromptInputBody>
<PromptInputTextarea />
</PromptInputBody>
<PromptInputFooter>
<span />
<PromptInputSubmit />
</PromptInputFooter>
</PromptInput>
</PromptInputProvider>
);
}Constrained image attachments
Reject everything except images and cap at 5 MB.
<PromptInput
accept="image/*"
multiple
maxFiles={4}
maxFileSize={5_000_000}
onError={(err) => toast.error(err.message)}
onSubmit={handle}
>
{/* ... */}
</PromptInput>Global drop zone
Accept files dropped anywhere on the page.
<PromptInput globalDrop accept="image/*" onSubmit={handle}>
{/* ... */}
</PromptInput>With Cmd+K command palette
Pair the inline composer with a global launcher using PromptInputCommandDialog.
const [open, setOpen] = useState(false);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((v) => !v);
}
};
document.addEventListener('keydown', handler);
return () => document.removeEventListener('keydown', handler);
}, []);
<PromptInputCommandDialog open={open} onOpenChange={setOpen}>
<PromptInputCommandInput placeholder="Type a command..." />
<PromptInputCommandList>
<PromptInputCommandEmpty>No results.</PromptInputCommandEmpty>
<PromptInputCommandGroup heading="Actions">
<PromptInputCommandItem>New chat</PromptInputCommandItem>
<PromptInputCommandItem>Clear history</PromptInputCommandItem>
</PromptInputCommandGroup>
</PromptInputCommandList>
</PromptInputCommandDialog>;Streaming status
Wire status to your network state to flip Submit between arrow, spinner and stop.
const [status, setStatus] = useState<ChatStatus>('ready');
async function send(message: PromptInputMessage) {
setStatus('submitted');
try {
await streamResponse(message, {
onStreamStart: () => setStatus('streaming'),
onComplete: () => setStatus('ready'),
});
} catch {
setStatus('error');
}
}
<PromptInputSubmit
status={status}
onClick={status === 'streaming' ? stop : undefined}
/>;Accessibility
- Real
<form>semantics:PromptInputrenders a<form>, so Enter submits viaform.requestSubmit(). HTML form validation still runs - nativerequired/patternattributes work on any field inside. - Submit
aria-label:PromptInputSubmitalways carriesaria-label="Submit". If your localization differs, override via your ownButtoncomposition. - Keyboard contract (documented above and enforced in code): Enter submits, Shift+Enter inserts a newline, Backspace on empty textarea removes the latest attachment. The IME composition flag (
onCompositionStart/onCompositionEnd) suppresses Enter while composing CJK input. - Speech button degradation: When the Web Speech API is missing,
PromptInputSpeechButtonis disabled rather than hidden.aria-label="Toggle voice input"stays on the disabled button so screen readers can describe what is unavailable. - Attachment remove: Each chip's remove button carries
aria-label="Remove attachment"plus ansr-only"Remove" span. - Tools needing tooltips: Icon-only
PromptInputButtons require anaria-label. There is no built-in tooltip, so wrap each inTooltipfrom@gremorie/rx-overlayswhen you want a hover hint. - Status announcement: When
statuschanges, consider announcing the new state via a separatearia-liveregion in the host app - Submit's icon swap alone is silent for screen readers. - Focus visible: The
InputGroupsurface drives the focus ring. Do not strip outlines. - Reduced motion: The speech button's pulse animation and the submit spinner respect
prefers-reduced-motionvia Tailwind's defaultmotion-reduce:variants (apply manually to custom additions).
Related
- Message - the assistant / user turn rendered above this composer
- Conversation - the scrollable surface that hosts both messages and this composer
- Model Selector - alternative standalone model picker if you do not want it inline
- Suggestion - pre-baked prompts users can click to seed the textarea
- Open in Chat - hand the current query off to an external chat