Hi! Ask me anything, attach a file, or drop one on the box.
PromptInput
A growing message composer with attachments, tools, a model picker and a send button that stops a streaming reply.
Chat
With MessageList: send, watch the status go submitted, streaming, ready, and stop a reply mid-stream.
Code
<MessageList busy={state.status === "streaming"}>
<Message e:for={m of state.messages} e:key={(m) => m.id} from={m.role}>...</Message>
</MessageList>
<PromptInput value={state.text} files={state.files} multiple onSubmit={(m) => send(state, m)}>
<PromptInputTools>
<PromptInputActionMenu>
<PromptInputActionAddAttachments />
<PromptInputActionAddScreenshot />
</PromptInputActionMenu>
<PromptInputButton toggle pressed={state.search} tooltip="Search the web"><Icon name="globe" /> Search</PromptInputButton>
<PromptInputSelect value={state.model} options={MODELS} />
</PromptInputTools>
<PromptInputSubmit status={state.status} onStop={() => stop(state)} />
</PromptInput>Usage
import { PromptInput, PromptInputTools, PromptInputButton, PromptInputSubmit, PromptInputActionMenu, PromptInputActionAddAttachments, PromptInputSelect } from "@elements/ui/prompt-input";
<PromptInput value={state.text} files={state.files} onSubmit={(m) => send(chat, m)}>
<PromptInputTools>
<PromptInputActionMenu>
<PromptInputActionAddAttachments />
</PromptInputActionMenu>
<PromptInputButton toggle pressed={state.search} tooltip="Search the web" shortcut="⌘K">
<Icon name="globe" /> Search
</PromptInputButton>
<PromptInputSelect value={state.model} options={MODELS} />
</PromptInputTools>
<PromptInputSubmit status={chat.status} onStop={() => stop(chat)} />
</PromptInput>Examples
Files
Pick, paste or drop files. accept, maxFiles and maxFileSize refuse what does not fit and report it through onError.
error: none submits: 0 · last: none
Code
<PromptInput value={state.text} files={state.files}
accept="image/*,.md" multiple maxFiles={3} maxFileSize={1_000_000}
onError={(err) => state.error = err.message}
onSubmit={(m) => save(m)}>
<PromptInputTools>
<PromptInputButton icon tooltip="Attach files" onclick={(e) => openPromptInputFileDialog(e.currentTarget)}>
<Icon name="paperclip" />
</PromptInputButton>
</PromptInputTools>
<PromptInputSubmit />
</PromptInput>Tools
The + menu, a toggle with a tooltip and shortcut, dictation and a model picker.
search: false · model: claude-opus · template picks: 0 · screenshot picks: 0
Code
<PromptInputTools>
<PromptInputActionMenu>
<PromptInputActionAddAttachments />
<PromptInputActionAddScreenshot />
<!-- e.preventDefault() in onSelect skips the capture. -->
<PromptInputActionAddScreenshot label="Screenshot (held back)" onSelect={(e) => e.preventDefault()} />
<PromptInputActionMenuItem onSelect={() => insertTemplate()}><Icon name="sparkles" /> Use a template</PromptInputActionMenuItem>
</PromptInputActionMenu>
<PromptInputButton toggle pressed={state.search} tooltip="Search the web" shortcut="⌘K"><Icon name="globe" /> Search</PromptInputButton>
<PromptInputSpeechButton />
<PromptInputSelect value={state.model} options={MODELS} />
</PromptInputTools>Styled model picker
A Select composes into PromptInputTools in place of the native PromptInputSelect. The prompt-input-select class gives its trigger the ghost look of the other tools; its popup opens from inside the form, and choosing a model never sends the prompt.
model: claude-opus · sent: 0
Code
<!-- Select instead of the native PromptInputSelect: prompt-input-select
gives its trigger the quiet ghost look; the popup opens from the form. -->
<PromptInput onSubmit={(m) => send(m)}>
<PromptInputTools>
<Select value={state.model} items={MODELS} label="Model" size="sm" position="popper" side="top"
class="prompt-input-select" />
</PromptInputTools>
<PromptInputSubmit />
</PromptInput>Async submit
A promise from onSubmit marks the form pending; it clears on resolve and keeps the text on reject. Type fail to see a rejection.
result: none
Code
// Resolve and it clears; reject and the text stays for a retry.
<PromptInput value={state.text} onSubmit={(m) => saveDraft(m)}>
<PromptInputSubmit />
</PromptInput>Submit button
PromptInputSubmit takes a size and an onclick that runs before the submit; e.preventDefault() in it cancels the send. Type draft to see it held back.
clicks: 0 · sent: 0
Code
<PromptInput value={state.text} onSubmit={(m) => send(m)}>
<PromptInputSubmit size="md" onclick={(e) => { track(); if (isDraft()) e.preventDefault(); }} />
</PromptInput>
<!-- Slotted content replaces the status icon. -->
<PromptInputSubmit label="Send"><Icon name="arrow-up" /></PromptInputSubmit>Cursor style
An @ hover card in the header searches files; picking one adds it as a referenced source and drops it from the list. The buttons below drive the input from outside.
Code
<PromptInput value={state.text} sources={state.sources} onSubmit={(m) => send(m)}>
<HoverCard slot="header" side="top" align="start">
<PromptInputButton slot="trigger" icon label="Add context"><Icon name="at-sign" /></PromptInputButton>
<Command label="Files">
<CommandInput placeholder="Search files..." />
<CommandList>
<CommandGroup heading="Files">
<CommandItem e:for={p of state.files} onSelect={() => addContext(state, p)}>{p}</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</HoverCard>
<PromptInputTools>
<PromptInputSelect value={state.model} options={MODELS} />
</PromptInputTools>
<PromptInputSubmit status={state.status} />
</PromptInput>
removePromptInputSource(el, id); clearPromptInputSources(el);
addPromptInputFiles(el, [file]); removePromptInputFile(el, id); clearPromptInputFiles(el); clearPromptInput(el);Sources and suggestions
Referenced sources ride along as chips. Tabs parts lay out suggestions in a hover card.
last: none
Code
<PromptInput value={state.text} sources={state.sources} onSubmit={(m) => ask(m.text, m.sources)}>
<PromptInputTools>
<HoverCard openDelay={0} closeDelay={0} align="start">
<PromptInputButton slot="trigger" icon label="Mention a source"><Icon name="at-sign" /></PromptInputButton>
<PromptInputTabsList>
<PromptInputTab>
<PromptInputTabLabel>Recently used</PromptInputTabLabel>
<PromptInputTabBody>
<PromptInputTabItem><Icon name="globe" /> Elements manual</PromptInputTabItem>
</PromptInputTabBody>
</PromptInputTab>
</PromptInputTabsList>
</HoverCard>
</PromptInputTools>
<PromptInputSubmit />
</PromptInput>Options
globalDrop takes a file dropped anywhere on the page; submitOnEnter off makes Enter a newline; allowEmpty sends with nothing typed; autofocus focuses on mount.
sent: 0
Code
<!-- Drop a file anywhere on the page, not only on the box. -->
<PromptInput value={state.text} files={state.files} globalDrop multiple>...</PromptInput>
<!-- Enter breaks a line; only the button sends. -->
<PromptInput value={state.text} submitOnEnter={false}>...</PromptInput>
<!-- Send with nothing typed (a "continue" prompt). -->
<PromptInput value={state.text} allowEmpty onSubmit={() => next()}>...</PromptInput>
<!-- Focus the textarea when it mounts. -->
<PromptInput value={state.text} autofocus>...</PromptInput>Slash commands
Command in the header slot: search, arrow and Enter to pick a command into the prompt.
picked: none
Code
<PromptInput value={state.text}>
<Command slot="header" label="Slash commands" class="prompt-command">
<CommandInput placeholder="Search commands..." />
<CommandList>
<CommandEmpty>No commands.</CommandEmpty>
<CommandGroup heading="Commands">
<CommandItem onSelect={() => insert("/summarize ")}>/summarize</CommandItem>
</CommandGroup>
</CommandList>
</Command>
<PromptInputTools>
<PromptInputActionMenu icon="paperclip" label="Attach">
<PromptInputActionAddAttachments />
</PromptInputActionMenu>
</PromptInputTools>
<PromptInputSubmit />
</PromptInput>Textarea props
textareaAttrs reach the textarea (maxlength, aria-describedby); onTextareaKeyDown runs first and can cancel the built-in Enter. Here Enter adds a line, Cmd or Ctrl+Enter sends, and ArrowUp in an empty box recalls the last message.
sent: 0 · last: none
Code
<PromptInput textareaAttrs={{ maxlength: 280, "aria-describedby": "count" }}
onTextareaKeyDown={(e) => onKey(e)}> <!-- preventDefault skips Enter to send -->
...
</PromptInput>
<span id="count">{text.length} / 280</span>Disabled
A disabled fieldset disables the textarea and every control in it.
Code
<PromptInput disabled placeholder="Sign in to send messages">...</PromptInput>