Skip to main content
NachUI

Prompt Input

The composer. A growing textarea, file attachments, a toolbar and a submit button that knows the stream's state.

1'use client';
2
3import { useState } from 'react';
4import { PromptInput, type PromptInputMessage } from '@/components/ui/prompt-input';
5
6export function Default() {
7 const [sent, setSent] = useState<string | null>(null);
8
9 const handleSubmit = (message: PromptInputMessage) => {
10 setSent(message.text);
11 };
12
13 return (
14 <div className="flex w-full max-w-lg flex-col gap-3">
15 <PromptInput onSubmit={handleSubmit}>
16 <PromptInput.Body>
17 <PromptInput.Textarea placeholder="Ask about any component…" />
18 </PromptInput.Body>
19 <PromptInput.Footer>
20 <PromptInput.Tools>
21 <PromptInput.AddAttachments />
22 </PromptInput.Tools>
23 <PromptInput.Submit />
24 </PromptInput.Footer>
25 </PromptInput>
26 {sent && <p className="text-muted-foreground text-xs">Submitted: {sent}</p>}
27 </div>
28 );
29}

Installation

pnpm dlx nachui add prompt-input

Anatomy

1import { PromptInput } from '@/components/ui/prompt-input';
1<PromptInput onSubmit={(message) => sendMessage(message.text, message.files)}>
2 <PromptInput.Header>
3 <PromptInput.Attachments />
4 </PromptInput.Header>
5 <PromptInput.Body>
6 <PromptInput.Textarea placeholder="Ask anything…" />
7 </PromptInput.Body>
8 <PromptInput.Footer>
9 <PromptInput.Tools>
10 <PromptInput.AddAttachments />
11 </PromptInput.Tools>
12 <PromptInput.Submit status={status} />
13 </PromptInput.Footer>
14</PromptInput>
The root is a real form. Submitting calls onSubmit with { text, files } and then clears itself, which you can turn off with clearOnSubmit={false} if your app owns the reset.

Composition

Use the following composition to build a PromptInput:
PromptInput
├── PromptInput.Header
│ └── PromptInput.Attachments
├── PromptInput.Body
│ └── PromptInput.Textarea
└── PromptInput.Footer
├── PromptInput.Tools
│ ├── PromptInput.AddAttachments
│ └── PromptInput.Button
└── PromptInput.Submit

Keyboard

Enter sends, Shift and Enter make a newline, and composition input from an IME is left alone so an unconfirmed candidate never gets submitted by accident. The textarea grows with the text up to maxRows and then scrolls.

Files

Files arrive three ways: the clip button, a drop anywhere on the composer, and a drop anywhere on the page when globalDrop is set. accept, maxFiles and maxFileSize are checked before a file is kept, and anything rejected goes to onError with a code you can show.
1'use client';
2
3import { PromptInput } from '@/components/ui/prompt-input';
4
5export function Attachments() {
6 return (
7 <div className="w-full max-w-lg">
8 <PromptInput accept="image/*,.pdf,.md" multiple maxFiles={4} globalDrop>
9 <PromptInput.Header>
10 <PromptInput.Attachments />
11 </PromptInput.Header>
12 <PromptInput.Body>
13 <PromptInput.Textarea placeholder="Drop a file anywhere, or use the clip…" />
14 </PromptInput.Body>
15 <PromptInput.Footer>
16 <PromptInput.Tools>
17 <PromptInput.AddAttachments />
18 <PromptInput.Button label="Model">gpt-5</PromptInput.Button>
19 </PromptInput.Tools>
20 <PromptInput.Submit />
21 </PromptInput.Footer>
22 </PromptInput>
23 </div>
24 );
25}
Each accepted file becomes a PromptInputFile with an object URL ready to preview and the original File for upload:
1interface PromptInputFile {
2 id: string;
3 filename: string;
4 mediaType: string;
5 size: number;
6 url: string;
7 file: File;
8}
usePromptInputAttachments() exposes files, add, remove, clear and openFileDialog, which is how you build your own strip with Attachments instead of the built-in chips.

Status

status on the submit button covers the full round trip: ready sends, submitted spins, streaming turns into a stop square and error goes destructive.
1'use client';
2
3import { useState } from 'react';
4import { PromptInput, type PromptInputStatus } from '@/components/ui/prompt-input';
5
6const STATUSES: PromptInputStatus[] = ['ready', 'submitted', 'streaming', 'error'];
7
8export function Statuses() {
9 const [status, setStatus] = useState<PromptInputStatus>('ready');
10
11 return (
12 <div className="flex w-full max-w-lg flex-col gap-3">
13 <div className="flex flex-wrap gap-2">
14 {STATUSES.map((value) => (
15 <button
16 key={value}
17 type="button"
18 onClick={() => setStatus(value)}
19 data-active={status === value ? '' : undefined}
20 className="border-border hover:bg-muted data-active:bg-muted rounded-full border px-3 py-1 text-xs transition-colors"
21 >
22 {value}
23 </button>
24 ))}
25 </div>
26 <PromptInput>
27 <PromptInput.Body>
28 <PromptInput.Textarea placeholder="The submit button reflects the status…" />
29 </PromptInput.Body>
30 <PromptInput.Footer>
31 <PromptInput.Tools>
32 <PromptInput.AddAttachments />
33 </PromptInput.Tools>
34 <PromptInput.Submit status={status} disabled={status !== 'ready'} />
35 </PromptInput.Footer>
36 </PromptInput>
37 </div>
38 );
39}

API Reference

PromptInput

PropTypeDefaultDescription
onSubmit(message: PromptInputMessage, event: FormEvent) => void-Called with the text and the files
acceptstring-File types to accept
multiplebooleanfalseAllow more than one file
maxFilesnumber-Cap on attached files
maxFileSizenumber-Cap in bytes per file
globalDropbooleanfalseAccept a drop anywhere on the page
clearOnSubmitbooleantrueReset the text and the files after submit
onError(error: { code, message }) => void-Called when a file is rejected
classNamestring-Additional CSS classes

PromptInput.Textarea

PropTypeDefaultDescription
minRowsnumber1Rows before it grows
maxRowsnumber8Rows before it scrolls
classNamestring-Additional CSS classes

PromptInput.Submit

PropTypeDefaultDescription
status'ready' | 'submitted' | 'streaming' | 'error''ready'Icon, colour and disabling
labelstring'Send message'Accessible name
classNamestring-Additional CSS classes

PromptInput.Button

PropTypeDefaultDescription
labelstring-Accessible name and title
variant'ghost' | 'outline''ghost'Visual treatment
classNamestring-Additional CSS classes

PromptInput.AddAttachments

PropTypeDefaultDescription
labelstring'Add files'Accessible name
classNamestring-Additional CSS classes
PropTypeDefaultDescription
classNamestring-Additional CSS classes
Found something to improve?

Notice a bug, typo, or missing detail on this page? Help us make the documentation better by opening a GitHub issue.

Create an Issue