Prompt Input
A chat composer: a text box that grows with what is typed, up to a maximum height, and shrinks back once the message is sent. Enter sends and Shift+Enter adds a line; Enter that confirms an IME composition does neither. Send stays disabled for an empty or whitespace-only message, and a stop button appears while a reply is streaming. value is bindable, and sending is reported through onSubmit with the text as typed. Set maxLength for a counter that blocks sending once the limit is passed, suggestions for prompts shown under the box, and leading / trailing for controls of your own, such as voice. Files that are waiting to go out are passed in attachments: images show as thumbnails that open in a Lightbox, other files as chips, and each can be removed. Set onAttach to let files in: an attach button appears, and files can be pasted or dropped on the composer. They are reported, not added: uploading them and putting them in attachments is up to the caller. Set queue to let messages be sent while a reply is on its way: the component reports them through onSubmit like any other, and the caller decides when they go out. Built from Textarea, Button, Tooltip, Badge and Lightbox.
Sending and streaming
Send is disabled while the box is empty or only white space. Once a message goes out the box empties and shrinks back to one line, submitted spins the button, and streaming swaps it for stop. Drive status from whatever your chat client reports.
<div class="w-full max-w-xl">
<PromptInput
bind:value={message}
{status}
placeholder="Ask about your order…"
onSubmit={send}
onStop={stop}
/>
</div>Long messages
The box grows with the text up to a maximum height, then scrolls inside itself so the send button never leaves the screen. Shift+Enter adds a line; Enter sends.
<div class="w-full max-w-xl">
<PromptInput bind:value={long} onSubmit={noop} />
</div>States
disabled takes the whole composer out of play. submitted and streaming block sending while the shopper keeps typing. error puts the message under the box, marks it invalid and ties it to the box for screen readers.
We couldn't reach the assistant. Try again.
<div class="w-full max-w-xl space-y-6">
<PromptInput value="Not available right now" disabled onSubmit={noop} />
<PromptInput
value="Do you restock the pink polish?"
status="submitted"
onSubmit={noop}
/>
<PromptInput value="" status="streaming" onSubmit={noop} onStop={noop} />
<PromptInput
bind:value={failed}
status="error"
error="We couldn't reach the assistant. Try again."
onSubmit={noop}
/>
</div>Character limit
maxLength adds a counter that turns destructive once the limit is passed. It doesn't stop typing: the shopper can trim the message, and sending stays off until they do.
<div class="w-full max-w-xl space-y-6">
<PromptInput bind:value={near} maxLength={40} onSubmit={noop} />
<PromptInput bind:value={over} maxLength={40} onSubmit={noop} />
</div>Suggestions
Prompts to start from, under the box. fill puts the text in the box so it can be edited first; send sends it straight away. Both are disabled while a reply is on its way.
Sent: nothing yet
<div class="w-full max-w-xl space-y-6">
<PromptInput
bind:value={filled}
suggestions={['Track my order', 'Find a gift under $50', 'Return an item']}
onSubmit={noop}
/>
<div class="space-y-2">
<PromptInput
suggestions={['Track my order', 'Return an item']}
suggestionAction="send"
onSubmit={(text) => (sent = [...sent, text])}
/>
<p class="text-muted text-xs">
Sent: {sent.length ? sent.join(', ') : 'nothing yet'}
</p>
</div>
</div>Leading and trailing
The bar under the text takes your own controls, such as web search on the left and voice next to send. Icon-only buttons need an aria-label.
<div class="w-full max-w-xl">
<PromptInput onSubmit={noop}>
{#snippet leading()}
<Button variant="ghost" size="icon" aria-label="Search the web"
><Globe class="size-4" /></Button
>
{/snippet}
{#snippet trailing()}
<Button variant="ghost" size="icon" aria-label="Start voice input"
><Mic class="size-4" /></Button
>
{/snippet}
</PromptInput>
</div>Image thumbnails
Files waiting to go out sit above the box, in a list you bind to. An image with a url shows as a thumbnail that opens in a Lightbox, so the shopper can check the photo before sending it; any other file is a chip with its name. Each one has its own remove button.
- receipt.pdf
<div class="w-full max-w-xl">
<PromptInput
bind:attachments={thumbnails}
placeholder="Add a message…"
onSubmit={noop}
/>
</div>Attaching files
onAttach adds an attach button and lets files be pasted or dropped on the composer. It reports the files rather than adding them: upload them and push them into attachments yourself — here they are previewed from memory with URL.createObjectURL, which a real app swaps for the uploaded file's URL. accept keeps out the kinds you don't take, and says so. A message can go out with files and no text, and the list empties when it does.
Sent: nothing yet
<div class="w-full max-w-xl space-y-2">
<PromptInput
bind:value={attachedText}
bind:attachments={files}
accept="image/*,.pdf"
placeholder="Paste, drop or attach a photo…"
onAttach={(picked) => files.push(...attachmentsFrom(picked))}
onSubmit={(text, attached) =>
(delivered = `${attached.length} file(s)${text ? ` and “${text}”` : ''}`)}
/>
<p class="text-muted text-xs">Sent: {delivered ?? 'nothing yet'}</p>
</div>Queueing while streaming
With queue, send stays available while a reply streams, beside stop, and is labelled Queue message. The composer reports the message through onSubmit like any other, and your app holds it until the reply is done. The thread can show it with the queued status.
Queued: nothing yet
<div class="w-full max-w-xl space-y-2">
<PromptInput
bind:value={queueText}
status="streaming"
queue
placeholder="Type while the reply streams…"
onSubmit={(text) => queued.push(text)}
onStop={noop}
/>
<p class="text-muted text-xs">
Queued: {queued.length ? queued.join(' · ') : 'nothing yet'}
</p>
</div>API
PromptInput
A chat composer: a text box that grows with what is typed, up to a maximum height, and shrinks back once the message is sent. Enter sends and Shift+Enter adds a line; Enter that confirms an IME composition does neither. Send stays disabled for an empty or whitespace-only message, and a stop button appears while a reply is streaming. value is bindable, and sending is reported through onSubmit with the text as typed. Set maxLength for a counter that blocks sending once the limit is passed, suggestions for prompts shown under the box, and leading / trailing for controls of your own, such as voice. Files that are waiting to go out are passed in attachments: images show as thumbnails that open in a Lightbox, other files as chips, and each can be removed. Set onAttach to let files in: an attach button appears, and files can be pasted or dropped on the composer. They are reported, not added: uploading them and putting them in attachments is up to the caller. Set queue to let messages be sent while a reply is on its way: the component reports them through onSubmit like any other, and the caller decides when they go out. Built from Textarea, Button, Tooltip, Badge and Lightbox.
| Prop | Type | Default | Description |
|---|---|---|---|
value bindable | string | '' | The text typed and not yet sent. Two-way bindable via |
attachments bindable | import('./prompt-input-attachments.svelte').PromptInputAttachment[] | [] | Files waiting to be sent with the message, shown as chips. Two-way bindable via |
status | 'ready'|'submitted'|'streaming'|'error' | 'ready' | Where the reply is. |
queue | boolean | false | Allows sending while a reply is on its way. The message is reported through |
disabled | boolean | false | Disables the box, the send button and the suggestions. |
error | string | Message shown under the box; also marks the box invalid. | |
maxLength | number | Character limit. Shows a counter, and blocks sending once passed. | |
suggestions | string[] | [] | Prompts offered under the box. Choosing one never takes the pending attachments, and they are disabled while a reply is on its way even with |
suggestionAction | 'fill'|'send' | 'fill' | What choosing a suggestion does: put it in the box, or send it straight away. |
label | string | 'Message' | Accessible name of the box. |
placeholder | string | Placeholder text shown when the box is empty. | |
leading | import('svelte').Snippet | Controls before the send button, on the left of the bar. | |
trailing | import('svelte').Snippet | Controls next to the send button, on the right of the bar. | |
onAttach | (files: File[]) => void | Called with the files picked from the attach button, pasted or dropped. Shows the attach button; without it, files can't be added from the composer. | |
accept | string | The kinds of file | |
onSubmit required | (text: string, attachments: import('./prompt-input-attachments.svelte').PromptInputAttachment[]) => void | Called with the text as typed and the attachments when a message is sent. | |
onStop | () => void | Called when stop is pressed while a reply is streaming. | |
class | string | Additional Tailwind classes merged onto the root element. |
PromptInputAttachments
The files waiting to go out with a PromptInput message. An image with a url shows as a thumbnail that opens in a Lightbox; any other file shows as a chip with its name. Each one has a button to remove it.
| Prop | Type | Default | Description |
|---|---|---|---|
attachments required | PromptInputAttachment[] | The files, in the order they were added. | |
onremove required | (attachment: PromptInputAttachment) => void | Called when a remove button is pressed. | |
disabled | boolean | false | Disables the remove buttons. |
class | string | Additional Tailwind classes merged onto the list. |
PromptInputCounter
Character count for a PromptInput, shown as count / max. It turns destructive once the count passes the limit, so the overflow reads as a state and not only as a number.
| Prop | Type | Default | Description |
|---|---|---|---|
count required | number | Characters typed so far. | |
max required | number | The limit the count is measured against. | |
class | string | Additional Tailwind classes merged onto the badge. |
PromptInputSubmit
The send button of a PromptInput. While a reply is streaming a stop button appears, in place of send or, with queue, beside it. While the request is in flight send shows a spinner, unless messages can be queued. Both are icon-only, so each carries an aria-label and a tooltip.
| Prop | Type | Default | Description |
|---|---|---|---|
status | 'ready'|'submitted'|'streaming'|'error' | 'ready' | Where the reply is. |
disabled | boolean | false | Disables send. Stop stays available while streaming. |
queue | boolean | false | Messages can be sent while a reply is on its way, so send stays beside stop and never spins. |
onclick | () => void | Called when send is pressed. | |
onStop | () => void | Called when stop is pressed. |
PromptInputSuggestions
A row of suggested prompts for a PromptInput. Each one is a button that hands its text to onselect; the parent decides whether that fills the input or sends it.
| Prop | Type | Default | Description |
|---|---|---|---|
suggestions required | string[] | The prompts to offer, in order. | |
onselect required | (suggestion: string) => void | Called with the text of the chosen suggestion. | |
disabled | boolean | false | Disables every suggestion. |
class | string | Additional Tailwind classes merged onto the group. |
PromptInputAttachment
| Prop | Type | Default | Description |
|---|---|---|---|
id required | string | Stable key for the attachment. | |
name required | string | File name. | |
type required | string | MIME type, such as | |
url | string | Where an image can be previewed from. |