PromptInput
freePromptInput combines autosizing message entry, attachments, command selection, and explicit leading and trailing toolbars.
Preview
Dark
import { useState } from 'react';
import PromptInput from '@/components/composites/PromptInput';
import Button from '@/components/ui/Button';
import { PiSlidersHorizontal } from 'react-icons/pi'
export default function UsageDemo() {
const [submitted, setSubmitted] = useState('');
return (
<div className="space-y-2 w-full">
<PromptInput
placeholder="Write a message"
onSubmit={(value) => setSubmitted(value)}
>
<PromptInput.Toolbar>
<PromptInput.ToolbarStart>
<PromptInput.AttachButton />
</PromptInput.ToolbarStart>
<PromptInput.ToolbarEnd>
<Button
type="button"
variant="ghost"
shape="circle"
size="sm"
icon={<PiSlidersHorizontal />}
aria-label="Message options"
/>
<PromptInput.Submit />
</PromptInput.ToolbarEnd>
</PromptInput.Toolbar>
</PromptInput>
{submitted && (
<p className="text-sm text-muted-foreground">
Submitted: {submitted}
</p>
)}
</div>
);
}Installation
Add this component with the NateUI CLI.
npx nateui@latest add PromptInputExamples
Compact
Compact mode starts as a one-row composer and expands into the stacked layout after a line wraps or an attachment is present.
Preview
Dark
import PromptInput from '@/components/composites/PromptInput';
export default function CompactDemo() {
return (
<div className="w-full">
<PromptInput
layout="compact"
placeholder="Add a follow-up"
onSubmit={() => undefined}
>
<PromptInput.Toolbar>
<PromptInput.ToolbarStart>
<PromptInput.AttachButton />
</PromptInput.ToolbarStart>
<PromptInput.ToolbarEnd>
<PromptInput.Submit />
</PromptInput.ToolbarEnd>
</PromptInput.Toolbar>
</PromptInput>
</div>
);
}Attachments
The built-in attach action reads selected browser files as data URLs, shows removable previews above the text area, and returns them as the second submit argument.
Preview
Dark
import { useState } from 'react';
import PromptInput from '@/components/composites/PromptInput';
const initialAttachments = [
{
id: 'brief',
name: 'brief.png',
mediaType: 'image/png',
url: '/img/thumbs/misc/img-1.png',
},
];
export default function AttachmentsDemo() {
const [attachments, setAttachments] = useState(initialAttachments);
return (
<div className="w-full">
<PromptInput
attachments={attachments}
onAttachmentsChange={setAttachments}
placeholder="Add context"
onSubmit={() => setAttachments([])}
>
<PromptInput.Toolbar>
<PromptInput.ToolbarStart>
<PromptInput.AttachButton />
</PromptInput.ToolbarStart>
<PromptInput.ToolbarEnd>
<PromptInput.Submit />
</PromptInput.ToolbarEnd>
</PromptInput.Toolbar>
</PromptInput>
</div>
);
}Slash Commands
Pass plain command records. Typing / filters the list; arrow keys move the active row, Enter inserts the command token, and the parent receives the selected record.
Preview
Dark
import { useState } from 'react';
import PromptInput from '@/components/composites/PromptInput';
import { PiFileText, PiLightning, PiMagnifyingGlass } from 'react-icons/pi'
const commands = [
{
id: 'summarize',
label: 'Summarize',
description: 'Turn the current note into a short summary',
icon: <PiFileText />,
keywords: ['brief'],
},
{
id: 'search',
label: 'Search',
description: 'Find related material',
icon: <PiMagnifyingGlass />,
},
{
id: 'run',
label: 'Run check',
description: 'Start the selected workflow',
icon: <PiLightning />,
},
];
export default function SlashCommandsDemo() {
const [selected, setSelected] = useState('');
return (
<div className="space-y-2 w-full">
<PromptInput
commands={commands}
placeholder="Type / to choose a command"
onCommandSelect={(command) => setSelected(command.label)}
onSubmit={() => undefined}
/>
{selected && (
<p className="text-sm text-muted-foreground">
Selected: {selected}
</p>
)}
</div>
);
}Busy And Stop
Preview
Dark
Thinking...
import { useState } from 'react';
import Shimmer from '@/components/ui/Shimmer'
import PromptInput from '@/components/composites/PromptInput';
import Button from '@/components/ui/Button';
export default function BusyAndStopDemo() {
const [busy, setBusy] = useState(true);
return (
<div className="space-y-4 w-full">
{busy && <Shimmer active>Thinking...</Shimmer>}
<PromptInput
status={busy ? 'busy' : 'idle'}
onSubmit={() => setBusy(true)}
onStop={() => setBusy(false)}
placeholder="Queue another message"
/>
</div>
);
}API
PromptInput
| Prop | Description | Type | Default |
|---|---|---|---|
onSubmit | Receives the trimmed message and current attachments. | (value: string, attachments: AttachmentData[]) => void | — |
layout | Uses a stacked composer or a single-row compact start state. | 'stacked' | 'compact' | 'stacked' |
attachments | Controlled attachment records rendered above the field. | AttachmentData[] | — |
onAttachmentsChange | Receives selected and removed attachment records. | (attachments: AttachmentData[]) => void | — |
attachmentAccept | Native file-input accept filter. | string | — |
commands | Records filtered when the cursor is in a / token. | PromptInputCommand[] | [] |
onCommandSelect | Called after a command token is inserted. | (command: PromptInputCommand) => void | — |
onStop | Called by PromptInput.Submit while status="busy". | () => void | — |
status | Switches the submit control between send and stop. | PromptInputStatus | 'idle' |
value | Current controlled message value. Pair with onChange. | string | — |
onChange | Receives each message-value change. | (value: string) => void | — |
placeholder | Text shown when the message field is empty. | string | 'Message…' |
ref | Exposes clear() and focus() methods. | Ref<PromptInputRef> | — |
...formProps | Standard <form> attributes forwarded to the root. | ComponentProps<'form'> | — |
PromptInput.Toolbar
Use one ToolbarStart and one ToolbarEnd inside each Toolbar. The end group contains PromptInput.Submit explicitly.
| Part | Description |
|---|---|
ToolbarStart | Leading actions, such as AttachButton or a caller-owned picker. |
ToolbarEnd | Trailing controls and Submit. |
AttachButton | Opens the native multi-file picker owned by the root composer. |
Submit | Sends the current text/attachments, or stops while busy. |
Types
type PromptInputLayout = 'stacked' | 'compact';
type PromptInputStatus = 'idle' | 'busy';
type PromptInputCommand = {
id: string;
label: string;
description?: ReactNode;
icon?: ReactNode;
keywords?: string[];
value?: string;
};