Prompt Input
An auto-growing composer with attachments by click, paste or drop, a token counter and a send button that becomes Stop.
Built on Icons.
Enter sends. Shift+Enter adds a line.
Installation
npx shadcn@latest add https://neonkit.heyaadi.com/r/prompt-input.jsonUsage
import { PromptInput } from "@/components/ui/prompt-input"
<PromptInput onSubmit={({ text, attachments }) => send(text, attachments)} streaming={streaming} onStop={stop} />Examples
Attachments
Attach with the clip, by pasting a screenshot or by dropping files on the box. accept, maxFiles and maxFileSize filter them and onRejectFiles says why.
Character limit
A counter that turns amber at 90% and red past the limit, where send switches off. submitOn="mod-enter" for longer writing.
Disabled
Everything switches off together. Say why next to it.
API reference
Renders a form; extra props go on it. The ref points at the textarea so you can focus it.
| Prop | Type | Default |
|---|---|---|
value | stringControlled text. | None |
defaultValue | stringInitial text when uncontrolled. | "" |
onValueChange | (value: string) => voidCalled on every edit. | None |
onSubmit | (message: { text, attachments }) => voidText is trimmed. Uncontrolled text and attachments clear afterwards. | None |
placeholder | stringPlaceholder text. | "Write a message" |
label | stringAccessible name of the textarea. | "Message" |
minRows / maxRows | numberThe textarea grows between these, then scrolls. | 1 / 8 |
submitOn | "enter"Enter sends and Shift+Enter breaks the line, or ⌘/Ctrl+Enter sends and Enter breaks it. | "enter" |
streaming | booleanSend becomes Stop; Escape stops too. | false |
onStop | () => voidCalled by Stop and by Escape while streaming. | None |
disabled | booleanDisables the field, the buttons, paste and drop. | false |
attachments | PromptAttachment[]Controlled attachments: { id, name, size?, type?, url?, file? }. | None |
defaultAttachments | PromptAttachment[]Initial attachments when uncontrolled. | [] |
onAttachmentsChange | (attachments) => voidCalled when files are added or removed. | None |
allowAttachments | booleanThe clip button, paste and drop. | true |
accept | stringAllowed types, as in <input accept>. | None |
maxFiles | numberFiles past this are rejected with reason "count". | None |
maxFileSize | numberBytes per file. | None |
onRejectFiles | (files: File[], reason: "type" | "size" | "count") => voidCalled with the files that didn't make it. | None |
toolbar | ReactNodeControls after the clip: a model picker, tool toggles. | None |
counter | "characters"Show a running count next to send. | None |
limit | numberUpper bound for the counter. Over it, send is disabled. | None |
countTokens | (text: string) => numberSwap in a real tokenizer for exact counts. | length / 4 |
color | NeonColorPalette name or hex. Falls back to --neon-accent, then cyan. | inherited |
Accessibility
- Enter is ignored while an IME composition is open, so confirming Japanese, Chinese or Korean input never sends half a word.
- The textarea is labelled and points at the counter with aria-describedby; going over the limit sets aria-invalid.
- Send is a real submit button; it's disabled until there's text or a file. Stop is a separate button named Stop.
- Each attachment chip has a Remove button named after the file. Removing one returns focus to the textarea.
- Image previews use object URLs that are revoked when the chip is removed or the composer unmounts; sent ones are left for you to show.
- Under prefers-reduced-motion the send button lights without the flicker.