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);
4
5 React.useEffect(() => () => clearTimeout(timerRef.current), []);
6
7 return (
8 <div style={{ width: 420 }}>
9 <PromptInput
10 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'
2
3<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 Skills
9 </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

A slot row above the textarea, usually for Chat.Attachment previews. Hidden while empty.

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.Attachment
5 title="design-spec.pdf"
6 description="1.2 MB"
7 onRemove={() => {}}
8 />
9 <Chat.Attachment
10 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 calls onStop.
  • streaming — tokens are arriving; the button flips to a stop square that calls onStop, 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"];
3
4 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 <PromptInput
12 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("");
3
4 return (
5 <Flex direction="column" gap={4} style={{ width: 420 }}>
6 <PromptInput
7 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 <Button
6 type="button"
7 variant="ghost"
8 color="neutral"
9 size="small"
10 disabled
11 >
12 Skills
13 </Button>
14 <PromptInput.Submit />
15 </PromptInput.Footer>

Accessibility

  • The root is a native <form>; PromptInput.Submit is 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.