The message box of a chat: one rounded bar with a + menu on the left, the text in the middle, and your own controls, a microphone and Send on the right. Added files show above the text. You get only the buttons you ask for.
Bring your own model.
These components only show what you give them, in one message shape, and report what the person does. They never call a model or a server, so they work with any backend. They are built for how assistive technology handles streaming text, tool steps and approvals.
Enter to send, Shift+Enter for a new line
import { useState } from "react";
import { PromptInput } from "@rdloom/react";
// The simplest form: just a box and a Send button. Add the other buttons by passing the props for them.
export default function PromptInputExample() {
const [sent, setSent] = useState<string[]>([]);
return (
<div className="flex w-[36rem] max-w-full flex-col gap-3">
<PromptInput placeholder="Ask anything" onSubmit={(text) => setSent((s) => [...s, text])} />
{sent.length > 0 && (
<ul className="flex flex-col gap-1 text-sm">
{sent.map((text, i) => (
<li key={i}>You sent: {text}</li>
))}
</ul>
)}
</div>
);
}Installation
npx rdloom add prompt-inputCopies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes. It needs react-aria-components; add --install to install them.
Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/prompt-input.jsonWorks, but without upgrade tracking.
Usage
import { PromptInput } from "@rdloom/react";
<div className="flex w-[36rem] max-w-full flex-col gap-3">
<PromptInput placeholder="Ask anything" onSubmit={(text) => setSent((s) => [...s, text])} />
{sent.length > 0 && (
<ul className="flex flex-col gap-1 text-sm">
{sent.map((text, i) => (
<li key={i}>You sent: {text}</li>
))}
</ul>
)}
</div>With attachments
Enter to send, Shift+Enter for a new line
import { useEffect, useRef, useState } from "react";
import { PromptInput, fileToAttachment, releaseAttachment, type PromptAttachment } from "@rdloom/react";
// onAttach turns on "Add photos & files" in the + menu. You own the list: add to it, remove from it, and clear it
// when the message is sent. fileToAttachment makes a small preview for pictures; releaseAttachment frees it.
export default function PromptInputWithAttachmentsExample() {
const [files, setFiles] = useState<PromptAttachment[]>([]);
const [sent, setSent] = useState<string[]>([]);
const latest = useRef(files);
latest.current = files;
useEffect(() => () => latest.current.forEach(releaseAttachment), []);
return (
<div className="flex w-[36rem] max-w-full flex-col gap-3">
<PromptInput
placeholder="Ask anything"
accept="image/*,.pdf,.txt"
attachments={files}
onAttach={(chosen) => setFiles((list) => [...list, ...chosen.map(fileToAttachment)])}
onRemoveAttachment={(id) =>
setFiles((list) => {
list.filter((a) => a.id === id).forEach(releaseAttachment);
return list.filter((a) => a.id !== id);
})
}
onSubmit={(text, attached) => {
setSent((s) => [...s, `${text || "(no text)"} + ${attached.length} file${attached.length === 1 ? "" : "s"}`]);
setFiles([]); // the files went with the message
}}
/>
{sent.length > 0 && (
<ul className="flex flex-col gap-1 text-sm">
{sent.map((line, i) => (
<li key={i}>You sent: {line}</li>
))}
</ul>
)}
</div>
);
}With tools
Enter to send, Shift+Enter for a new line
Nothing chosen yet.
import { useState } from "react";
import { GlobeIcon, ImageIcon, PromptInput, SparkleIcon } from "@rdloom/react";
// The + menu can hold your own tools next to "Add photos & files". endContent puts a control of yours on the
// right, and onVoice adds the microphone button.
export default function PromptInputWithToolsExample() {
const [listening, setListening] = useState(false);
const [think, setThink] = useState(false);
const [note, setNote] = useState("Nothing chosen yet.");
return (
<div className="flex w-[36rem] max-w-full flex-col gap-3">
<PromptInput
placeholder="Ask anything"
onAttach={() => setNote("You chose Add photos & files.")}
actions={[
{ id: "image", label: "Create image", description: "Visualize anything", icon: <ImageIcon className="size-5" />, onSelect: () => setNote("You chose Create image.") },
{ id: "search", label: "Web search", description: "Find real-time news and info", icon: <GlobeIcon className="size-5" />, onSelect: () => setNote("You chose Web search.") },
]}
onVoice={() => setListening((v) => !v)}
isListening={listening}
endContent={
<button
type="button"
aria-pressed={think}
onClick={() => setThink((v) => !v)}
className="flex h-10 items-center gap-1.5 rounded-full px-3 text-sm text-[var(--rd-color-text-muted)] outline-none hover:bg-[var(--rd-color-surface-selected)] hover:text-[var(--rd-color-text-default)] focus-visible:ring-2 focus-visible:ring-[var(--rd-color-focus-ring)] aria-pressed:bg-[var(--rd-color-surface-selected)] aria-pressed:text-[var(--rd-color-action-primary)]"
>
<SparkleIcon className="size-4" />
Think
</button>
}
onSubmit={(text) => setNote(`You sent: ${text}${think ? " (thinking on)" : ""}`)}
/>
<p role="status" className="text-sm text-[var(--rd-color-text-muted)]">
{note}
{listening && " Listening…"}
</p>
</div>
);
}Streaming
Enter to send, Shift+Enter for a new line
import { useState } from "react";
import { PromptInput } from "@rdloom/react";
// While a reply is on its way the Send button becomes Stop and sending is blocked.
export default function PromptInputStreamingExample() {
const [streaming, setStreaming] = useState(true);
return (
<div className="w-[36rem] max-w-full">
<PromptInput placeholder="Ask anything" isStreaming={streaming} onStop={() => setStreaming(false)} onSubmit={() => setStreaming(true)} />
</div>
);
}API Reference
Defined by the spec. Components also accept the props of the React Aria component they wrap.
| Prop | Type | Default |
|---|---|---|
onSubmitrequiredCalled with the trimmed text and the attachments when the person sends. A message with files and no text can be sent. | (text: string, attachments: PromptAttachment[]) => void | none |
onStopCalled when the person presses Stop. | () => void | none |
isStreamingA reply is on its way: the button becomes Stop and sending is blocked. | boolean | false |
isDisabledStop all input. | boolean | false |
placeholderHint shown while the box is empty. | string | "Message the assistant" |
labelAccessible name of the text box. | string | "Message" |
valueControlled text. Leave out to let the box keep its own. | string | none |
onValueChangeCalled as the text changes. | (value: string) => void | none |
maxLengthLongest message allowed. | number | none |
attachmentsFiles added to the message but not sent yet. You own the list: add to it in onAttach, remove from it in onRemoveAttachment. Images show as small pictures, other files as chips. | PromptAttachment[] | none |
onAttachCalled with the chosen files. Providing it adds "Add photos & files" to the + menu and opens the file picker. Use fileToAttachment() to make attachments. | (files: File[]) => void | none |
onRemoveAttachmentCalled with an attachment's id when the person removes it. Without it the attachments have no remove button. | (id: string) => void | none |
acceptWhich files the picker offers, e.g. "image/*,.pdf". | string | none |
actionsExtra entries for the + menu, such as Create image or Web search. Each has a label, an optional description and icon, and what to do when chosen. | PromptAction[] | none |
onVoiceShows a microphone button and calls this when it is pressed. Start or stop dictation yourself. | () => void | none |
isListeningDictation is on: the microphone button shows as pressed and its name becomes Stop dictation. | boolean | false |
endContentYour own controls on the right, before the microphone, for example a Think toggle. | node | none |
Accessibility
Role textbox, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.
Keyboard
- Enter: sends
- Shift+Enter: new line
- Tab: moves through the + button, the text, your controls, the microphone and Send
- In the + menu: arrow keys move, Enter chooses, Escape closes
Screen readers announce
- "Message, edit text, multi-line. Enter to send, Shift+Enter for a new line"
- "Add attachments or tools, menu button", then the entries such as "Add photos & files"
- After choosing files: "Added photo.png"; the list reads "Attachments, list, 1 item" and "Remove photo.png, button"
- "Send message, button, dimmed" while empty; "Stop generating, button" while streaming
What your code must do
- A real labelled multi-line text box; the shortcut hint is attached with aria-describedby (read by assistive technology, not drawn)
- Enter does nothing while an input method (IME) is composing, so a half-typed word can't be sent
- Send is disabled when there is no text and no file; Stop replaces it while streaming and has its own name
- The + button is a menu button named "Add attachments or tools"; its menu has a name and every entry has a text label
- Attachments are a labelled list; every remove button names its file ("Remove photo.png"); adding and removing is announced politely; an image's alt text is its file name
- Only pictures from this device (blob: or data: addresses) are drawn: nothing is loaded from a remote address
- The microphone button is a toggle (aria-pressed) whose name changes to Stop dictation
- When the text grows past a line the bar becomes two rows without moving the text box in the page, so typing and focus are never interrupted
- Focus stays in the text box after sending, attaching or removing a file
- Growth stops at a maximum height, then the box scrolls
Guidelines
Use it when
- Any place a person writes to an assistant
Avoid it when
- A short single-line field: use TextField
Don't
- Sending on Enter without checking for IME composition
- Clearing the box before the message is sent
Design tokens
The semantic tokens this component uses. Change them once and every component follows; see Design tokens.
--rd-color-action-on-primary--rd-color-action-primary--rd-color-action-primary-hover--rd-color-border-default--rd-color-border-strong--rd-color-focus-ring--rd-color-surface-default--rd-color-surface-raised--rd-color-surface-selected--rd-color-surface-subtle--rd-color-text-default--rd-color-text-muted--rd-elevation-control--rd-elevation-floating--rd-elevation-raised--rd-radius-overlay