"use client";
import { GlobeIcon } from "lucide-react";
import { useRef, useState } from "react";
import {
PromptInput,
PromptInputActionAddAttachments,
PromptInputActionMenu,
PromptInputActionMenuContent,
PromptInputActionMenuTrigger,
PromptInputAttachment,
PromptInputAttachments,
PromptInputBody,
PromptInputButton,
PromptInputFooter,
type PromptInputMessage,
PromptInputProvider,
PromptInputSpeechButton,
PromptInputSubmit,
PromptInputTextarea,
PromptInputTools,
usePromptInputController,
} from "@/components/ui/prompt-input";
import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
const models = [
{
id: "gpt-4o",
name: "GPT-4o",
chef: "OpenAI",
chefSlug: "openai",
providers: ["openai", "azure"],
},
{
id: "gpt-4o-mini",
name: "GPT-4o Mini",
chef: "OpenAI",
chefSlug: "openai",
providers: ["openai", "azure"],
},
{
id: "claude-opus-4-20250514",
name: "Claude 4 Opus",
chef: "Anthropic",
chefSlug: "anthropic",
providers: ["anthropic", "azure", "google", "amazon-bedrock"],
},
{
id: "claude-sonnet-4-20250514",
name: "Claude 4 Sonnet",
chef: "Anthropic",
chefSlug: "anthropic",
providers: ["anthropic", "azure", "google", "amazon-bedrock"],
},
{
id: "gemini-2.0-flash-exp",
name: "Gemini 2.0 Flash",
chef: "Google",
chefSlug: "google",
providers: ["google"],
},
];
const SUBMITTING_TIMEOUT = 200;
const STREAMING_TIMEOUT = 2000;
const HeaderControls = () => {
const controller = usePromptInputController();
return (
<header className="mt-8 flex items-center justify-between">
<p className="text-sm">
Header Controls via{" "}
<code className="rounded-md bg-muted p-1 font-bold">
PromptInputProvider
</code>
</p>
<ButtonGroup>
<Button
onClick={() => {
controller.textInput.clear();
}}
size="sm"
type="button"
variant="outline"
>
Clear input
</Button>
<Button
onClick={() => {
controller.textInput.setInput("Inserted via PromptInputProvider");
}}
size="sm"
type="button"
variant="outline"
>
Set input
</Button>
<Button
onClick={() => {
controller.attachments.clear();
}}
size="sm"
type="button"
variant="outline"
>
Clear attachments
</Button>
</ButtonGroup>
</header>
);
};
const PromptInputExample = () => {
const [model, setModel] = useState<string>(models[0].id);
const [modelSelectorOpen, setModelSelectorOpen] = useState(false);
const [status, setStatus] = useState<
"submitted" | "streaming" | "ready" | "error"
>("ready");
const textareaRef = useRef<HTMLTextAreaElement>(null);
const selectedModelData = models.find((m) => m.id === model);
const handleSubmit = (message: PromptInputMessage) => {
const hasText = Boolean(message.text);
const hasAttachments = Boolean(message.files?.length);
if (!(hasText || hasAttachments)) {
return;
}
setStatus("submitted");
// eslint-disable-next-line no-console
console.log("Submitting message:", message);
setTimeout(() => {
setStatus("streaming");
}, SUBMITTING_TIMEOUT);
setTimeout(() => {
setStatus("ready");
}, STREAMING_TIMEOUT);
};
return (
<div className="size-full">
<PromptInputProvider>
<PromptInput globalDrop multiple onSubmit={handleSubmit}>
<PromptInputAttachments>
{(attachment) => <PromptInputAttachment data={attachment} />}
</PromptInputAttachments>
<PromptInputBody>
<PromptInputTextarea ref={textareaRef} />
</PromptInputBody>
<PromptInputFooter>
<PromptInputTools>
<PromptInputActionMenu>
<PromptInputActionMenuTrigger />
<PromptInputActionMenuContent>
<PromptInputActionAddAttachments />
</PromptInputActionMenuContent>
</PromptInputActionMenu>
<PromptInputSpeechButton textareaRef={textareaRef} />
<PromptInputButton>
<GlobeIcon size={16} />
<span>Search</span>
</PromptInputButton>
</PromptInputTools>
<PromptInputSubmit status={status} />
</PromptInputFooter>
</PromptInput>
<HeaderControls />
</PromptInputProvider>
</div>
);
};
export PromptInputExample;
Overview
PromptInput is a composable form wrapper for AI chats. It handles text, attachments, and submit behavior while letting you control the exact layout with compound subcomponents. It is built on top of the InputGroup primitives and is fully compatible with shadcn/ui styling.
This component is pretty similar to AI Elements Prompt Input, but I added some improvements which I wanted, and removed uncessary complexity for my use cases.
You can use it as a self-contained component, or lift its state with PromptInputProvider so external buttons, menus, or presets can read/write the same input state.
Installation
bunx --bun shadcn@latest add https://sidekick.montek.dev/r/prompt-input.jsonUsage
Basic
import {
PromptInput,
type PromptInputMessage,
} from "@/components/ui/prompt-input";
export function ChatInput() {
const handleSubmit = async ({ text, files }: PromptInputMessage) => {
if (!text.trim()) return;
// Send text + files to your API route
await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text, files }),
});
};
return (
<PromptInput onSubmit={handleSubmit}>
<PromptInput.Body>
<PromptInput.Textarea placeholder="Ask me anything..." />
</PromptInput.Body>
<PromptInput.Footer>
<PromptInput.Tools />
<PromptInput.Submit />
</PromptInput.Footer>
</PromptInput>
);
}Attachments + Action Menu
<PromptInput
accept="image/*"
multiple
maxFiles={4}
onError={(err) => console.log(err.message)}
onSubmit={handleSubmit}
>
<PromptInput.Attachments>
{(file) => <PromptInput.Attachment data={file} />}
</PromptInput.Attachments>
<PromptInput.Body>
<PromptInput.Textarea
minRows={3}
placeholder="Drop files or paste images..."
/>
</PromptInput.Body>
<PromptInput.Footer>
<PromptInput.Tools className="gap-2">
<PromptInput.ActionMenu>
<PromptInput.ActionMenuTrigger />
<PromptInput.ActionMenuContent>
<PromptInput.ActionAddAttachments />
<PromptInput.ActionMenuItem>Summarize</PromptInput.ActionMenuItem>
</PromptInput.ActionMenuContent>
</PromptInput.ActionMenu>
</PromptInput.Tools>
<PromptInput.Submit />
</PromptInput.Footer>
</PromptInput>External Controls with Provider
Use PromptInputProvider when you want to control the same input from siblings (preset chips, floating toolbars, etc.).
import {
PromptInput,
PromptInputProvider,
usePromptInputController,
} from "@/components/ui/prompt-input";
function Presets() {
const controller = usePromptInputController();
return (
<div className="flex flex-wrap gap-2">
<button
className="rounded-full border px-3 py-1 text-xs"
onClick={() => controller.textInput.setInput("Summarize the call")}
type="button"
>
Recap
</button>
<button
className="rounded-full border px-3 py-1 text-xs"
onClick={() => controller.textInput.setInput("Draft a follow-up email")}
type="button"
>
Follow-up
</button>
</div>
);
}
export function ProviderExample() {
return (
<PromptInputProvider initialInput="Give me a quick update.">
<Presets />
<PromptInput shape="pill" size="sm" onSubmit={handleSubmit}>
<PromptInput.Body>
<PromptInput.Textarea />
<PromptInput.SpeechButton />
</PromptInput.Body>
<PromptInput.Footer align="inline-end">
<PromptInput.Tools>
<PromptInput.Button className="gap-1 px-3">
<SparklesIcon className="size-4" />
<span>Rewrite</span>
</PromptInput.Button>
</PromptInput.Tools>
<PromptInput.Submit />
</PromptInput.Footer>
</PromptInput>
</PromptInputProvider>
);
}Sleeves for Top/Bottom Sections
Use Sleeve components to create styled top or bottom sections that wrap around your prompt input, perfect for context chips, suggestions, or related UI.
import { PromptInput } from "@/components/ui/prompt-input";
export function SleeveExample() {
return (
<div className="mx-auto max-w-2xl">
<PromptInput.TopSleeve className="bg-muted/50 p-3">
<div className="flex flex-wrap gap-2">
<span className="text-muted-foreground text-xs">Suggested:</span>
<button className="rounded-full border px-2 py-1 text-xs hover:bg-accent">
Explain this code
</button>
<button className="rounded-full border px-2 py-1 text-xs hover:bg-accent">
Add tests
</button>
</div>
</PromptInput.TopSleeve>
<PromptInput variant="outline" onSubmit={handleSubmit}>
<PromptInput.Body>
<PromptInput.Textarea placeholder="What would you like to know?" />
</PromptInput.Body>
<PromptInput.Footer>
<PromptInput.Tools />
<PromptInput.Submit />
</PromptInput.Footer>
</PromptInput>
<PromptInput.BottomSleeve className="bg-muted/30 p-2">
<p className="text-center text-muted-foreground text-xs">
Press Enter to send, Shift+Enter for new line
</p>
</PromptInput.BottomSleeve>
</div>
);
}Behavior
- Enter to submit (Shift+Enter for a new line).
- Paste images/files directly into the textarea.
- Backspace on empty input removes the last attachment.
- Drag and drop files onto the input (or anywhere with
globalDrop). - Attachment payloads: file attachments are stored as
blob:URLs while editing, then converted to data URLs on submit.
API Reference
PromptInput
The root form container. Accepts standard form props plus the options below.
| Prop | Type | Description |
|---|---|---|
variant | default | ghost | outline | none | Controls the visual border and background style. Use none when wrapping with PromptInput.Card. |
shape | default | pill | Controls the border-radius. pill creates a fully rounded input. |
size | default | sm | lg | Controls height, padding, and text size. sm is compact/single-line optimized. |
focusRing | boolean | Toggles the input focus ring styling. |
accept | string | MIME types allowed (ex: image/*). |
multiple | boolean | Allow multiple files in the picker. |
globalDrop | boolean | Accept drops anywhere on the document instead of just the input. |
maxFiles | number | Maximum number of files allowed. |
maxFileSize | number | Maximum file size in bytes. |
syncHiddenInput | boolean | Deprecated. File inputs cannot be set programmatically; prefer a custom submit payload. |
onError | (err) => void | Called on max file/size/accept errors. |
onSubmit | (message, event) => void | Promise<void> | Called with { text, files } after submit (files are converted to data URLs when needed). |
PromptInputMessage
type PromptInputMessage = {
text: string;
files: FileUIPart[];
};PromptInput.Card
Optional wrapper component for custom card-style containers.
| Prop | Type | Description |
|---|---|---|
variant | default | muted | Controls the background color intensity. |
shape | default | pill | Controls the border-radius. |
PromptInputProvider
Optional context provider that lifts text + attachment state outside of PromptInput.
Subcomponents
- PromptInput.Body: Flex container for the textarea + inline buttons.
- PromptInput.Textarea: Auto-resizing textarea with paste/file handling.
- PromptInput.Header / PromptInput.Footer: Toolbar containers. Pass
alignto toggle inline/stacked placement. - PromptInput.Tools: Wrapper for grouped buttons or chip lists.
- PromptInput.Button / PromptInput.Submit / PromptInput.SpeechButton: Composable buttons built on top of
InputGroupButton. - PromptInput.Attachments / PromptInput.Attachment: File preview list plus individual attachment chips.
- PromptInput.ActionMenu / ActionMenuTrigger / ActionMenuContent / ActionMenuItem: Dropdown menu primitives.
- PromptInput.ActionAddAttachments: Pre-wired menu item to open the file picker.
- PromptInput.Select / SelectTrigger / SelectContent / SelectItem / SelectValue: Select primitives styled for toolbars.
- PromptInput.HoverCard / HoverCardTrigger / HoverCardContent: Hover card primitives for attachment previews.
- PromptInput.TabsList / Tab / TabLabel / TabBody / TabItem: Lightweight tab primitives for quick presets.
- PromptInput.Sleeve / TopSleeve / BottomSleeve: Decorative wrapper components for top/bottom sections around the input.