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 <PromptInput.Button>Skills</PromptInput.Button>
8 <PromptInput.Submit />
9 </PromptInput.Footer>
10</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.

Button

A small neutral-ghost Button for the toolbar — attach, skills, model pickers and similar consumer-owned controls.

Prop

Type

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…" />

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 <PromptInput.Button>Skills</PromptInput.Button>
6 <PromptInput.Submit />
7 </PromptInput.Footer>
8 </PromptInput>
9</div>

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 (:focus-within), while the borderless textarea suppresses its own duplicate focus ring.