Prompt Input

The foundational input component for AI interfaces. Built on Shadcn UI principles with compound composition, attachments, and speech controls.

components/ui/prompt-input.demo.tsx
"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.json

Usage

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.

PropTypeDescription
variantdefault | ghost | outline | noneControls the visual border and background style. Use none when wrapping with PromptInput.Card.
shapedefault | pillControls the border-radius. pill creates a fully rounded input.
sizedefault | sm | lgControls height, padding, and text size. sm is compact/single-line optimized.
focusRingbooleanToggles the input focus ring styling.
acceptstringMIME types allowed (ex: image/*).
multiplebooleanAllow multiple files in the picker.
globalDropbooleanAccept drops anywhere on the document instead of just the input.
maxFilesnumberMaximum number of files allowed.
maxFileSizenumberMaximum file size in bytes.
syncHiddenInputbooleanDeprecated. File inputs cannot be set programmatically; prefer a custom submit payload.
onError(err) => voidCalled 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.

PropTypeDescription
variantdefault | mutedControls the background color intensity.
shapedefault | pillControls 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 align to 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.