Skip to main content

Composer

The box under a chat, in one piece. Prompt input, attachments, suggestions, a model picker and the context meter, wired to a single onSend.

1'use client';
2
3import { useState } from 'react';
4import { Composer, type ComposerMessage } from '@/components/ui/composer';
5
6const MODELS = [
7 { id: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash', description: 'Fast, cheap, most turns' },
8 { id: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro', description: 'Slower, for hard questions' },
9 { id: 'claude-sonnet-5', label: 'Claude Sonnet 5', description: 'Long context, careful' },
10];
11
12const SUGGESTIONS = ['What is NachUI?', 'Install the CLI', 'Show me a chat example'];
13
14export function Default() {
15 const [model, setModel] = useState(MODELS[0]?.id ?? '');
16 const [sent, setSent] = useState<ComposerMessage | null>(null);
17
18 return (
19 <div className="flex w-full max-w-lg flex-col gap-3">
20 <Composer
21 onSend={setSent}
22 suggestions={SUGGESTIONS}
23 models={MODELS}
24 model={model}
25 onModelChange={setModel}
26 context={{ used: 12400, max: 128000 }}
27 placeholder="Ask about any component…"
28 />
29 {sent && (
30 <p className="text-muted-foreground text-xs">
31 Sent “{sent.text}” with {sent.files.length} file{sent.files.length === 1 ? '' : 's'}
32 </p>
33 )}
34 </div>
35 );
36}

Installation

pnpm dlx nachui add composer

Anatomy

1import { Composer } from '@/components/ui/composer';
1<Composer
2 onSend={({ text, files }) => send(text, files)}
3 onStop={stop}
4 status={status}
5 suggestions={['What is NachUI?', 'Install the CLI']}
6 models={models}
7 model={model}
8 onModelChange={setModel}
9 context={{ used: 12400, max: 128000 }}
10/>
With no children the composer lays itself out: suggestions and attachment chips above, the textarea in the middle, attach and model on the left of the footer, the context meter and the send button on the right. Enter sends, Shift+Enter breaks the line, Escape clears.
Pass children to rearrange the parts, drop one or put something of yours between them. Every part reads from the same context, so the order is yours and the wiring stays.
1<Composer onSend={handleSend}>
2 <Composer.Attachments />
3 <Composer.Input />
4 <Composer.Footer>
5 <Composer.Send />
6 </Composer.Footer>
7</Composer>

Composition

A hybrid is visible composition. Open composer.tsx and these are the elements it is made of, in this order:
Composer
├── PromptInput
│ ├── Composer.Suggestions → Suggestion.Group + Suggestion
│ ├── Composer.Attachments → Attachments (inline)
│ ├── Composer.Input → PromptInput.Textarea
│ └── Composer.Footer
│ ├── PromptInput.AddAttachments
│ ├── DropdownMenu (model picker)
│ ├── Context (token meter)
│ └── Composer.Send → PromptInput.Submit or a stop button

Suggestions

suggestions renders a row of chips above the input while it is empty. A click sends the suggestion as a message. Set sendOnSuggestion={false} to fill the input instead and let the reader edit before sending.

Models

models adds a picker to the footer. It is a plain DropdownMenu showing the current label; picking another entry calls onModelChange with its id. The composer never stores the model, you do.

Context

context renders the Context meter next to the send button, with used and max tokens. Hover or focus it for the breakdown header.

Streaming

status follows the PromptInput statuses. While it is streaming the send button becomes a stop button that calls onStop, and onSend is ignored until the answer ends. submitted shows the spinner and blocks sending too.

Ready. The send button turns into stop for three seconds after you send.

1'use client';
2
3import { useEffect, useRef, useState } from 'react';
4import { Composer, type ComposerStatus } from '@/components/ui/composer';
5
6export function Streaming() {
7 const [status, setStatus] = useState<ComposerStatus>('ready');
8 const [last, setLast] = useState<string | null>(null);
9 const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
10
11 useEffect(() => {
12 return () => {
13 if (timer.current) clearTimeout(timer.current);
14 };
15 }, []);
16
17 const stop = () => {
18 if (timer.current) clearTimeout(timer.current);
19 setStatus('ready');
20 };
21
22 return (
23 <div className="flex w-full max-w-lg flex-col gap-3">
24 <Composer
25 status={status}
26 onSend={(message) => {
27 setLast(message.text);
28 setStatus('streaming');
29 timer.current = setTimeout(() => setStatus('ready'), 3000);
30 }}
31 onStop={stop}
32 placeholder="Send something and watch the button…"
33 />
34 <p className="text-muted-foreground text-xs">
35 {status === 'streaming'
36 ? `Streaming an answer to “${last}”. Stop cuts it short.`
37 : 'Ready. The send button turns into stop for three seconds after you send.'}
38 </p>
39 </div>
40 );
41}

Files

The attach button opens the file dialog and dropping files on the composer works too. Files show as inline chips with a remove button, and onSend receives them as a plain File[]. Limit them with maxFiles and accept.

API Reference

Composer

PropTypeDefaultDescription
onSend(message: { text: string; files: File[] }) => void-Called on Enter, the send button or a suggestion
onStop() => void-Called by the stop button while streaming
status'ready' | 'submitted' | 'streaming' | 'error''ready'Drives the send button and blocks sending
placeholderstring'Ask anything…'Textarea placeholder
disabledbooleanfalseDisables every control
maxFilesnumber-Cap on attached files
acceptstring-Accepted file types, as on an input
suggestionsstring[][]Chips shown while the input is empty
sendOnSuggestionbooleantrueSend on click, or fill the input
models{ id: string; label: string; description?: string }[][]Entries of the model picker
modelstring-Id of the current model
onModelChange(model: string) => void-Called with the picked id
context{ used: number; max: number }-Renders the token meter
labelsPartial<{ send; stop; attach; model }>EnglishAccessible names of the buttons
valuestring-Controlled text
defaultValuestring-Initial text when uncontrolled
onValueChange(value: string) => void-Called as the text changes
classNamestring-Additional CSS classes

Composer.Suggestions

PropTypeDefaultDescription
classNamestring-Additional CSS classes

Composer.Attachments

PropTypeDefaultDescription
classNamestring-Additional CSS classes

Composer.Input

PropTypeDefaultDescription
classNamestring-Additional CSS classes
...--Any textarea prop except value and onChange

Composer.Footer

PropTypeDefaultDescription
childrenReactNode<Composer.Send />What sits at the far right
classNamestring-Additional CSS classes

Composer.Send

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