PromptInput
A composer for AI chat and standalone "Ask anything" boxes — auto-growing textarea, toolbar slots, and a status-aware submit button.1(function PromptInputPreview() {2 const [status, setStatus] = React.useState("idle");3 const timerRef = React.useRef(null);45 React.useEffect(() => () => clearTimeout(timerRef.current), []);67 return (8 <div style={{ width: 420 }}>9 <PromptInput10 status={status}11 onSubmit={(value, event) => {12 event.currentTarget.reset();13 setStatus("streaming");14 timerRef.current = setTimeout(() => setStatus("idle"), 2500);15 }}
Anatomy
Import and assemble the composer. The root is a form that owns submit semantics: Enter submits, Shift+Enter inserts a newline, and IME composition is respected.
1import { PromptInput } from '@raystack/apsara'23<PromptInput onSubmit={send} onStop={stop} status={status}>4 <PromptInput.Header>{/* attachment previews */}</PromptInput.Header>5 <PromptInput.Textarea placeholder="Reply…" />6 <PromptInput.Footer>7 <Button type="button" variant="ghost" color="neutral" size="small">8 Skills9 </Button>10 <PromptInput.Submit />11 </PromptInput.Footer>12</PromptInput>
PromptInput is purely presentational: it never talks to a model or manages a
request. Pass the lifecycle in through status and react to onSubmit /
onStop — it works with the AI SDK's useChat, a custom SSE client, or
anything else.
API Reference
Root
The <form> that holds the value and submit semantics.
Prop
Type
Textarea
The text field, built on TextArea variant="borderless". It grows with its
content up to a max-height cap (override with
--prompt-input-textarea-max-height), then scrolls.
Prop
Type
Header
A slot row above the textarea, usually for Chat.Attachment previews. Hidden
while empty.
Footer
The bottom toolbar row. Hidden while empty. Toolbar controls — attach, skills,
model pickers and similar — are composed from regular Apsara components
(Button, IconButton, Select, …); give them type="button" so they don't
submit the form.
Submit
The trailing send button. It renders ↑ when idle, disables itself while the
input is empty, shows a spinner while status="submitted", and flips to a
stop square that calls onStop while status="streaming".
Prop
Type
Examples
Attachments in the header
Attachment previews are presentational (Chat.Attachment); file picking,
drag-drop and uploads belong to your app.
1<div style={{ width: 420 }}>2 <PromptInput onSubmit={(value) => console.log(value)}>3 <PromptInput.Header>4 <Chat.Attachment5 title="design-spec.pdf"6 description="1.2 MB"7 onRemove={() => {}}8 />9 <Chat.Attachment10 title="screenshot.png"11 state="uploading"12 description="Uploading…"13 />14 </PromptInput.Header>15 <PromptInput.Textarea placeholder="Reply…" />
Status
status reflects the request lifecycle your app manages:
idle— the resting state; the submit button shows the send arrow and disables itself while the input is empty.submitted— the request is sent but nothing has streamed back yet; the button shows a spinner, Enter/submit is blocked, and clicking callsonStop.streaming— tokens are arriving; the button flips to a stop square that callsonStop, and submit stays blocked.error— the request failed; the composer returns to a submittable state (send arrow) so the user can retry. Render your error message outside the composer.
1(function PromptInputStatuses() {2 const STATUSES = ["idle", "submitted", "streaming", "error"];34 return (5 <Flex gap={5} wrap="wrap">6 {STATUSES.map((status) => (7 <Flex key={status} direction="column" gap={2} style={{ width: 320 }}>8 <Text size="small" variant="secondary">9 status="{status}"10 </Text>11 <PromptInput12 status={status}13 defaultValue="Summarize this thread"14 onSubmit={() => {}}15 onStop={() => {}}
Controlled value
Control value to sync the draft elsewhere — persistence, slash-command
menus, mention pickers.
1(function ControlledPromptInput() {2 const [value, setValue] = React.useState("");34 return (5 <Flex direction="column" gap={4} style={{ width: 420 }}>6 <PromptInput7 value={value}8 onValueChange={setValue}9 onSubmit={() => setValue("")}10 >11 <PromptInput.Textarea placeholder="Write a message…" />12 <PromptInput.Footer>13 <PromptInput.Submit />14 </PromptInput.Footer>15 </PromptInput>
Disabled
1<div style={{ width: 420 }}>2 <PromptInput disabled>3 <PromptInput.Textarea placeholder="Read-only conversation" />4 <PromptInput.Footer>5 <Button6 type="button"7 variant="ghost"8 color="neutral"9 size="small"10 disabled11 >12 Skills13 </Button>14 <PromptInput.Submit />15 </PromptInput.Footer>
Accessibility
- The root is a native
<form>;PromptInput.Submitis a real submit button, so assistive tech announces the submit affordance for free. - Enter submits and Shift+Enter inserts a newline. During IME composition Enter confirms the composition instead of submitting.
- While a response is in flight the submit button becomes
type="button"with an updated accessible name ("Stop response") so it can never resubmit the form. - The composer frame carries the focus treatment — the border tints when a
child has visible focus (
:has(:focus-visible)) — while the borderless textarea suppresses its own duplicate focus ring.