Prompt Input
Compose a message with attachments and a send button that becomes a stop control.
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use it below a Chat Message List for anything someone writes to an assistant. It handles the keyboard rules people expect from a chat composer, attachments by picker, paste and drop, and the switch from send to stop while a reply runs. For a plain multi-line field in a form, use a Textarea.
import { PromptInput } from '@youj/react'
<PromptInput status={status} onStop={stop} onSubmit={({ text, attachments }) => send(text, attachments)} />onSubmit gets the trimmed text and the attachments. Uncontrolled, the field clears itself after sending. Control it with value and onValueChange to fill it from elsewhere, like a suggestion, and textareaRef to focus it.
Enter sends and Shift+Enter breaks the line, and neither cuts an IME composition short. On touch keyboards Enter is a line break and the button sends. The field grows with the message up to 40% of the viewport, then scrolls.
Description
A composer with a model select and a Search toggle, where send turns into stop while a pretend reply streams.
status is idle, submitted or streaming. Anything but idle turns send into stop, a real path morph from the arrow to a square, and Escape in the field stops too. Send stays focusable while it can't act, so keyboard focus is never dropped mid-reply.
toolbar puts your controls at the start of the bottom bar: a model select, tool toggles, dictation. The demo has a model Select and a Toggle. Size them sm to sit on the bar.
A composer that attaches files by paperclip, paste or drop, shown as chips above the text.
Pass onAttach and a paperclip appears, and pasting or dropping files attaches them too. accept filters all three, as in <input accept>. Attachments sit above the text as chips, a thumbnail for images and name and size for files, each with its own remove button. usePromptAttachments() manages picked files for you: add, remove and clear, with object-URL previews revoked when they're removed or the component unmounts.
An attachment with status: 'uploading' blocks sending until it's ready, and one with status: 'error' says so.
A 220-character limit with a long draft in the field, so the counter shows.
maxLength shows a counter near the limit and blocks sending past it. The counter appears from 80% of the limit, turns to a warning at 95% and to danger past it. Replace it with counter, for example <PromptInputCounter value={tokens} max={8000} unit="tokens" /> to count tokens instead.
An assistant composer in a narrow side panel, with suggestion chips above it.
Chat Suggestions above the field fill it on a click and move focus there. onAttach brings the built-in paperclip; toolbar takes your own tools beside it, here a Create button and a dictation Toggle. maxLength blocks sending past 300 characters and shows the count near the limit.
A composer with a session panel under it: context used, reasoning, tools, MCP and skills, and a status dot, over a footer with workspace and permission selects and a spinner.
It is a composition, not a part: a Property List for the rows, a Segmented Meter for the context window (amber from 80%), Selects and a Spinner in the footer. Send a message: Status turns to Running with the spinner on, and the context grows by a turn when the reply ends.
Code
prompt-input-demo.tsx, the Demo story. The component itself comes from the package.
Reference
PropsonSubmit, value, defaultValue… (23)
Props
| Prop | Type | |
|---|---|---|
value | string | – |
defaultValue | stringdefault '' | '' |
onValueChange | (value: string) => void | – |
onSubmitRequired | (message: PromptSubmission) => voidCalled with the trimmed text and the attachments. Uncontrolled, the text then clears itself. | – |
status | PromptStatus'idle''submitted''streaming''submitted' and 'streaming' turn send into stop. default 'idle' | 'idle' |
onStop | () => void | – |
placeholder | stringdefault "Ask anything…" | "Ask anything…" |
label | stringThe text field's name for assistive tech. default "Message" | "Message" |
disabled | booleandefault false | false |
attachments | PromptAttachment[]default [] | [] |
onAttach | (files: File[]) => voidEnables attaching: the paperclip, paste and drop. Receives the files. | – |
onRemoveAttachment | (id: string) => void | – |
accept | stringWhich files the picker offers and paste or drop accept, as in <input accept>. | – |
toolbar | ReactNodeControls at the start of the bottom bar: a model select, tool toggles. | – |
counter | ReactNodeReplaces the built-in character counter, e.g. | – |
maxLength | numberA character limit: shows a counter near it and blocks sending past it. | – |
autoFocus | boolean | – |
textareaRef | Ref<HTMLTextAreaElement>The textarea, e.g. to focus it after picking a suggestion. | – |
Other props pass through: extends ComponentProps<'form'>. | ||
PromptInputCounter
| Prop | Type | |
|---|---|---|
valueRequired | number | – |
maxRequired | number | – |
unit | stringWhat's counted, for the announcement. default "characters" | "characters" |
from | numberShow from this share of default 0.8 | 0.8 |
id | string | – |
MotionSend to stop, reversal, attachment enters… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
Send to stopIconSwap morph: the arrow’s strokes bend into the square, and back | ease.emphasizedcubic-bezier(0.6, 0, 0.1, 1) · deliberate + fast: 700ms | cubic-bezier(0.6, 0, 0.1, 1)deliberate + fast: 700ms | |
| Reversalstopping mid-morph turns back from where it is | no token length × distance left | length × distance left | |
| Attachment entersfades and drops 6px (Presence List) | duration.baseease.enter220ms · cubic-bezier(0.16, 1, 0.3, 1) | 220mscubic-bezier(0.16, 1, 0.3, 1) | |
| Attachment leavesfades in place, then the rest close the gap | duration.fastease.exit140ms · cubic-bezier(0.4, 0, 1, 1) | 140mscubic-bezier(0.4, 0, 1, 1) | |
| Chips make room | spring.smooth0.36s, bounce 0 | 0.36s, bounce 0 | |
| Counterfades in near the limit | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) |
Reduced motion
Send and stop swap instantly, and chips appear and disappear in place.
Reduced motion
The arrow swaps to the square without morphing, and attachment chips appear and leave at once. Everything else, the keyboard rules and the counter, is unchanged.
AccessibilityARIA, focus, announcements
Accessibility
- The field is named by
label(default "Message"). WithmaxLength, it's described by the counter and setaria-invalidpast the limit. - The counter is announced only in the last stretch (under 10% left) and when over, not on every keystroke.
- Send is named "Send message", and "Stop generating" while busy, with a tooltip. It stays focusable while it can't act.
- The paperclip is named "Attach files". Attachments are a list named "Attachments", and each remove button is named after its file. Removing one moves focus to its neighbor, never to the page.
disabledgrays the whole frame and blocks typing, attaching and sending.