Where a message to an AI assistant is written: it grows as you type, takes files, and turns send into stop while the reply streams.
$ pnpm dlx shadcn@latest add https://dashral.com/r/prompt-composer.jsonInstallation
$ pnpm dlx shadcn@latest add https://dashral.com/r/prompt-composer.jsonAdds the component, the components it builds on, its npm packages and the theme tokens it uses. Using Dashral often? Add the registry to components.json once:
{
"registries": {
"@dashral": "https://dashral.com/r/{name}.json"
}
}Then add components by name:
$ pnpm dlx shadcn@latest add @dashral/prompt-composerUsage
import { PromptComposer } from "@/components/dashral/prompt-composer"
<PromptComposer
onSubmit={({ text, files }) => api.chat.send(text, files)}
attachments={{ accept: "image/*,.pdf", maxFiles: 4 }}
/>Examples
Attachments
From the paperclip, a pasted screenshot or files dropped on the box, checked for type, size and count.
API reference
PromptComposer
| Prop | Type | Default |
|---|---|---|
value / defaultValue | stringThe text, controlled or initial. Uncontrolled, it clears itself after sending. | "" |
onValueChange | (value: string) => voidCalled with the new value. | — |
onSubmit | ({ text, files }) => voidCalled with the message (text trimmed) when it's sent. | — |
status | "ready" | "submitted" | "streaming"While it's not ready, send becomes stop and Enter doesn't send. | "ready" |
onStop | () => voidThe stop button and Escape call it. | — |
submitOn | "enter" | "mod-enter"Enter sends and Shift + Enter adds a line, or only ⌘/Ctrl + Enter sends. | "enter" |
attachments | boolean | { accept?, maxFiles?, maxSize? }Files from the paperclip, a paste or a drop; true takes any file. | — |
files / defaultFiles / onFilesChange | File[]The attached files, controlled or initial. | [] |
maxLength | numberA count shows near it; past it the message can't be sent. | — |
actions | ReactNodeYour controls in the toolbar, after the paperclip: a model picker, a toggle. | — |
placeholder | string | "Ask anything…" |
disabled / required / autoFocus / id / name | — | |
label | ReactNodeShown above, linked to the control. | — |
description | ReactNodeA hint under the control. | — |
error | boolean | ReactNodetrue shows the error style; a message also shows under the control. | — |
labelAction | ReactNodeAt the end of the label's row, e.g. a “Forgot password?” link or the current value. | — |
withAsterisk | booleanMarks the label with an asterisk, e.g. alongside required. | false |
classNames | { root, label, description, error, composer, textarea, files, toolbar, submit }Classes for each part. | — |
className | stringClasses for the composer's box. | — |
Keyboard
| Keys | Action |
|---|---|
| Enter | Send (with submitOn "enter") |
| Shift + Enter | A new line |
| ⌘/Ctrl + Enter | Send, with either submitOn |
| Escape | Stop the reply while it streams |
| Tab | The paperclip, your controls, then send or stop |
Accessibility
- With label, description or error, the id, aria-describedby and aria-invalid are wired up for you.
- Without a label the text box is named "Message"; the buttons are named Send message, Stop generating and Attach files.
- Enter waits while a word is still being composed (Japanese, Chinese or Korean input), so it doesn't send half a word.
- Attached files are a list, each with a remove button that names the file; refused files are listed in an alert with the reason.
- Near the limit, a live region says how many characters are left, without reading every keystroke.
Motion
- The border and focus ring ease over --duration-fast; the count fades in near the limit. A drop shows its message at once.