Prompt Input / Composer
Preview: Card
Chat composer: auto-growing textarea, Enter to send (Shift+Enter for newline), attachments, Stop while streaming. IME-safe: Enter during composition (e.g. Japanese input) never sends.
Import
import { PromptInput } from '@marbleui/ui'Props
<PromptInput>
| Prop | Type | Default | Description |
|---|---|---|---|
| onSubmit *required | (text: string, attachments: PromptAttachment[]) => void | — | Gets the trimmed text and the attachments, each with its `File`. Make your own object URL from `file` if you show it after sending. |
| allowAttachments | boolean | true | |
| className | string | — | |
| defaultValue | string | '' | |
| isStreaming | boolean | — | While true the send button becomes Stop. |
| maxRows | number | 8 | |
| onChange | (v: string) => void | — | |
| onStop | () => void | — | |
| placeholder | string | 'Ask anything…' | |
| tools | ReactNode | — | Controls on the left of the toolbar (model picker, tools). |
| value | string | — | |
| variant | 'card' | 'pill' | 'card' |
Accessibility
Keyboard interaction, focus management and ARIA semantics come from the underlying primitives: React Aria Button · React Aria FileTrigger. Follow the primitive's docs for the full keyboard map.
Usage notes
Chat composer: auto-growing textarea, Enter to send, Shift+Enter for a newline, attachments, and a Stop button while streaming.
<PromptInput onSubmit={(text, files) => send(text, files)} isStreaming={busy} onStop={abort} />
onSubmit(text, attachments): each attachment has the pickedFileasfile, ready to upload. Image previews use object URLs that are revoked once the attachment is sent or removed, so make your own URL fromfileif you show it after sending.value/defaultValue/onChangecontrol the text.- IME-safe: Enter during composition (Japanese, Chinese input) doesn’t submit.
toolsadds controls to the footer (model picker, web search toggle).- Pair with
ChatConversationandChatMessagefor the transcript.
Example source · show· hide
import { useState } from 'react'
import { Button } from '../button'
import { PromptInput } from './prompt-input'
function Demo({ variant }: { variant: 'card' | 'pill' }) {
const [sent, setSent] = useState<string | null>(null)
const [streaming, setStreaming] = useState(false)
return (
<div className="flex w-[34rem] flex-col gap-3">
{sent && <div className="self-end rounded-2xl bg-surface-sunken px-3 py-2 text-sm text-fg">{sent}</div>}
<PromptInput
variant={variant}
isStreaming={streaming}
onStop={() => setStreaming(false)}
onSubmit={(t) => {
setSent(t)
setStreaming(true)
setTimeout(() => setStreaming(false), 2500)
}}
tools={variant === 'card' ? <Button size="sm" variant="ghost" className="rounded-full text-fg-muted">Search</Button> : undefined}
/>
</div>
)
}
export const styles = { Card: () => <Demo variant="card" />, Pill: () => <Demo variant="pill" /> }Source: packages/ui/src/prompt-input/prompt-input.tsx
Catalog
- Category
- ai-agents
- Priority
- Pro
- Primitive
- composed
- React Aria
- TextField + Toolbar