Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

AI Kit

Prompt Input

AI Kit

Prompt Input React component for Tailwind CSS v4

Model PickerReasoning Indicator
Live props
State
defaultValue
status
disabled
defaultModel
Content
placeholder
Appearance
variant
size
radius
tone
color
Layout
minRows1
maxRows8
position
safeArea
Behavior
submitShortcut
maxLengthNone
tokenLimitNone
showCount
allowAttachments
accept
maxFiles10
camera
voice
focusShortcut
stopShortcut
autoFocus
name
locale
Code
import { PromptInput } from "@/components/ui/prompt-input"

<PromptInput />
  1. Home
  2. Components
  3. AI Kit
  4. Prompt Input
Pro

Prompt Input React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

A React prompt input for AI chat: a growing text box with attachments, tool chips, a model picker, dictation and a send and stop button.

Type and the box grows up to maxRows, then scrolls. With a keyboard Enter sends and Shift+Enter makes a new line (with submitShortcut mod+enter, Enter is a new line and Cmd/Ctrl+Enter sends); an IME composition is confirmed by Enter, never sent. On a phone Enter always makes a new line and the round button sends without closing the keyboard. Press + for Upload files (and Take a photo on touch); drop files anywhere on the composer (a dashed overlay shows where) or paste a screenshot. Images show thumbnails, files a colored type tile; uploads show a progress ring, failures a retry button, and every attachment has a remove button (Backspace in an empty box jumps to the last one, Backspace again removes it). Tool chips toggle with a click (in a composer narrower than 512px, a phone or a side panel, they live in the + menu, a bottom sheet on phones, and active ones stay visible); the model name opens the model list (a bottom sheet on phones). The mic dictates at the caret: finished words land in the text, words still being heard stay muted in the voice pill. While the answer streams the button is Stop (and stopShortcut; a bare key like Escape works while focus is in the composer); ArrowUp in an empty box edits the last message; focusShortcut (e.g. Shift+Escape) focuses the box from anywhere. Past 80% of maxLength or tokenLimit a counter appears and turns red past the limit, which blocks sending.

Prompt Input is a Pro React component in the React AI components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add prompt-input, together with Textarea, Button, Toggle, Tooltip, Dropdown Menu and 18 more and the npm packages motion and lucide-react.

Add to a project

Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup

npx wingo-ui@latest add prompt-input

Prompt Input is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans

Usage

Import PromptInput from @/components/ui/prompt-input.

"use client"

import { useState } from "react"
import { Brain, Database, Globe, Sparkles, Gem } from "lucide-react"
import { PromptInput, type AiModel, type PromptInputMessage, type PromptInputStatus, type PromptTool } from "@/components/ui/prompt-input"

const MODELS: AiModel[] = [
  { id: "standard", name: "Acme Standard", group: "Acme AI", description: "Balanced, for everyday work", icon: <Sparkles />, recommended: true },
  { id: "pro", name: "Acme Pro", group: "Acme AI", description: "Deep reasoning for analysis and reports", icon: <Gem />, isNew: true },
]

const TOOLS: PromptTool[] = [
  { id: "web", label: "Web search", icon: <Globe /> },
  { id: "reasoning", label: "Extended thinking", icon: <Brain /> },
  { id: "crm", label: "CRM data", icon: <Database /> },
]

export function Composer({ ask, stop }: { ask: (message: PromptInputMessage) => Promise<void>; stop: () => void }) {
  const [status, setStatus] = useState<PromptInputStatus>("idle")

  async function send(message: PromptInputMessage) {
    setStatus("streaming")
    try {
      await ask(message)
    } finally {
      setStatus("idle")
    }
  }

  async function upload(file: File, { signal, onProgress }: { signal: AbortSignal; onProgress: (fraction: number) => void }) {
    const body = new FormData()
    body.append("file", file)
    onProgress(0.5)
    const response = await fetch("/api/files", { method: "POST", body, signal })
    if (!response.ok) throw new Error("Upload failed")
    return (await response.json()) as { url: string }
  }

  return (
    <PromptInput
      models={MODELS}
      tools={TOOLS}
      defaultActiveTools={["crm"]}
      voice
      status={status}
      onSubmit={send}
      onStop={stop}
      onUpload={upload}
      stopShortcut="escape"
      focusShortcut="shift+escape"
      labels={{ placeholder: "Ask the Acme assistant" }}
    />
  )
}

Props

  • defaultValuedefault ""
    string

    Starting text while uncontrolled. Use value / onValueChange to control it; a rejected onSubmit restores the text through onValueChange.

  • value
    string

    The text, controlled. Pair it with onValueChange.

  • onValueChange
    (value: string) => void

    The new text on every change, dictation and restores included.

  • onSubmit
    (message: PromptInputMessage) => (void | Promise<unknown>)

    Runs on Enter or the button when there is text or a ready attachment: { text (trimmed), attachments (ready or local), model, tools }. The composer clears at once and keeps focus; if the promise rejects, the text and files come back and an error is announced.

  • statusdefault "idle"
    "idle" | "submitting" | "streaming"

    submitting: the button spins; streaming: it becomes Stop. Sending is blocked while not idle, typing is not.

  • onStop
    () => void

    The Stop button and stopShortcut while status is streaming.

  • placeholderdefault "Ask anything"
    string

    Also the textarea's accessible name unless aria-labelledby is passed.

  • variantdefault "elevated"
    "elevated" | "outline" | "soft"

    elevated: a floating white panel with a soft shadow (ChatGPT); outline: a bordered field; soft: the filled well of the form fields.

  • sizedefault "md"
    "sm" | "md" | "lg"

    Text 14 / 15 / 16px (16px on phones always, so iOS never zooms), toolbar buttons 28 / 32 / 36px, padding 8 / 10 / 12px.

  • radiusdefault "lg"
    "md" | "lg" | "full"

    md 16px, lg 24px; full is a 28px pill while the text is one line and 24px once it grows or holds attachments.

  • tonedefault "neutral"
    "neutral" | "brand" | "success" | "warning" | "danger" | "info"

    The send button, active tool chips and the focus ring's tint. Brand orange is opt-in.

  • colordefault ""
    string

    Any CSS color; replaces the tone. Text on the send button turns black or white by lightness.

  • minRowsdefault 1
    number

    Height of the empty text area in rows.

  • maxRowsdefault 8
    number

    Grows with the text up to this many rows, then scrolls (native field-sizing, no JS per keystroke).

  • positiondefault "static"
    "static" | "sticky" | "fixed"

    fixed: pinned to the viewport bottom, lifted above the on-screen keyboard without re-rendering, safe-area padded, and writes its height as --prompt-input-offset on <html> so the page can pad its content; sticky: the bottom of its scroll parent (inside a chat thread). Both center the box in a 768px reading column over a fade.

  • safeAreadefault true
    boolean

    Pads the bottom by the home-indicator inset (at least 12px) when fixed or sticky.

  • submitShortcutdefault "enter"
    "enter" | "mod+enter"

    enter: Enter sends and Shift+Enter makes a new line on fine pointers (touch keyboards always insert a new line); mod+enter: Enter is a new line and Cmd/Ctrl+Enter sends. Cmd/Ctrl+Enter always sends.

  • maxLengthdefault 0
    number

    A soft character limit: the counter appears near it and turns red past it, and sending is blocked until the text is shorter. 0 = none.

  • tokenLimitdefault 0
    number

    Estimated tokens (estimateTokens): the hint reads "1,2 K / 8 K tokens" near the limit. 0 = none.

  • showCountdefault "auto"
    "auto" | "always" | "never"

    auto: the hint appears past 80% of maxLength / tokenLimit (warning), and turns danger past 100%; always: always shown (the token estimate without a limit); never: hidden, the limit still blocks.

  • allowAttachmentsdefault true
    boolean

    The + button, drop and paste of files.

  • acceptdefault "image/*,application/pdf,.doc,.docx,.xls,.xlsx,.csv,.txt,.md,.json"
    string

    Allowed types, like the input's accept attribute; checked by mime type and by extension. Defaults to DEFAULT_PROMPT_INPUT_ACCEPT.

  • maxFilesdefault 10
    number

    At most this many attachments; extra files are rejected with one message.

  • maxFileSizedefault 20971520
    number

    Bytes per file (20 MB).

  • cameradefault true
    boolean

    A "Take a photo" entry (capture="environment") in the + menu on touch devices.

  • voicedefault false
    boolean

    A dictation button (VoiceInput). Its transcript goes in at the caret, replacing a selection; while recording the voice pill takes the toolbar.

  • focusShortcutdefault ""
    string

    Focuses the box from anywhere and puts the caret at the end, e.g. "shift+escape". Empty binds nothing.

  • stopShortcutdefault ""
    string

    Stops a streaming reply, e.g. "escape". A bare key works while focus is in the composer (so a page dialog keeps its Escape), a combo like "mod+shift+x" works anywhere; an open menu or model list closes first. Empty binds nothing.

  • autoFocusdefault false
    boolean

    Focuses the box on mount on desktops only; a phone keyboard never pops up by surprise.

  • disableddefault false
    boolean

    Dims and blocks the box and every toolbar control.

  • namedefault ""
    string

    The textarea's name for forms.

  • localedefault ""
    string

    Number formatting of sizes, percentages and token counts (12.5K in en-US). Empty uses the UIProvider locale, then the browser's.

  • attachments
    ChatAttachment[]

    Controlled attachments ({ id, name, type, size, url, previewUrl, status, progress, error }). Pair it with onAttachmentsChange.

  • defaultAttachmentsdefault "[]"
    ChatAttachment[]

    Starting attachments while uncontrolled (a restored draft).

  • onAttachmentsChange
    (attachments: ChatAttachment[]) => void

    Every add, progress step, finish, failure and removal.

  • onUpload
    (file: File, { signal, onProgress }) => Promise<unknown>

    Uploads each added file with progress (0..1, one render per 100ms at most); resolve with a url string or { url } to set attachment.url. Removing a file aborts through the signal, Retry runs it again. Without it attachments stay local, with object-URL previews revoked on removal and unmount.

  • onRetry
    (attachment: ChatAttachment) => void

    Retry on a failed attachment the composer holds no File for (restored from a draft or added by the app).

  • models
    AiModel[]

    Shows a ModelPicker (ghost, opening upward; a bottom sheet on phones) in the toolbar.

  • model
    string

    The chosen model id, controlled. Uncontrolled it starts at defaultModel, else the first recommended model.

  • defaultModeldefault ""
    string

    Starting model id while uncontrolled. Empty starts at the first recommended model (Acme Standard in the demo).

  • onModelChange
    (model: string) => void

    A model was picked.

  • tools
    PromptTool[]

    Toggle chips in the toolbar ({ id, label, icon, description, disabled }); in a composer narrower than 512px they move into the + menu (a sheet on phones) as checkbox rows and only active ones stay in the toolbar; under 672px the chips show as round icons with the name in a tooltip.

  • activeTools
    string[]

    Active tool ids, controlled.

  • defaultActiveToolsdefault "[]"
    string[]

    Active tool ids while uncontrolled.

  • onActiveToolsChange
    (tools: string[]) => void

    A tool was switched on or off (ids in the order of tools).

  • onVoice
    (result: VoiceInputResult) => (void | Promise<unknown>)

    Makes the mic send audio messages (the blob and transcript) instead of dictating into the box.

  • onEditLast
    () => void

    ArrowUp in an empty box with the caret at the start: edit the last message.

  • toolbarStart
    ReactNode

    Extra controls after the model picker (a project picker).

  • toolbarEnd
    ReactNode

    Extra controls before the mic (a usage ring).

  • labels
    Partial<PromptInputLabels>

    Every string: placeholder, send, stop, attach, upload, photo, tools, cancel, attachments, remove(name), retry, uploading(name, percent), progress(percent), uploadFailed, drop, tooMany(max), tooLarge(name, max), typeNotAllowed(name), tokens(used, limit), tokenCount(used), characters(count, max), overLimit, newlineHint, attached(name), removed(name), sendFailed.

  • modelPickerProps
    Partial<ModelPickerProps>

    Passed to the model picker: labels, shortcut, groupBy, showSpeed.

  • voiceInputProps
    Partial<VoiceInputProps>

    Passed to the voice button: labels, lang, maxDuration, mode.

  • classNames
    Partial<Record<"box" | "textarea" | "attachments" | "attachment" | "toolbar" | "submit" | "hint" | "drop", string>>

    Extra classes for the inner parts.

  • className
    string

    Extra classes for the root, merged last. Other props (id, aria-*, data-*) go there too.

Parts

  • PromptInputAttachments

    component

    The attachment rail on its own: thumbnails and file tiles with size, progress ring, error and retry, and a remove button each; a horizontal rail with edge fades. Props: { attachments, onRemove?(attachment, { keyboard }), onRetry?, size?, locale?, disabled?, labels?, classNames?, ...ul }.

  • PromptInputSubmit

    component

    The round send button: an arrow, a spinner while submitting, a Stop square while streaming; dimmed but focusable (aria-disabled) with nothing to send. Props: { status?, canSubmit?, onStop?, size?, shortcut?, tooltip?, labels?, ...ButtonProps }.

  • PromptInputTool

    component

    A tool chip: an outline pill toggle with aria-pressed and an optional tooltip. Props: { label, icon?, description?, size?, ...ToggleProps }.

  • readAttachments

    function

    Validates picked, dropped or pasted files by type (mime or extension), size and count, with object-URL previews for images. Call it from an event handler. Signature: (files, { accept?, maxFiles?, maxFileSize?, current?, preview? }) => { accepted: ChatAttachment[]; rejected: { file; reason: "type" | "size" | "count" }[]; files: Map<string, File> }.

  • DEFAULT_PROMPT_INPUT_LABELS

    constant

    The English strings, to spread and override. Type: PromptInputLabels.

  • DEFAULT_PROMPT_INPUT_ACCEPT

    constant

    The default accept list: images, PDF, Word, Excel, CSV, plain text, Markdown and JSON. Extend it, e.g. `${DEFAULT_PROMPT_INPUT_ACCEPT},.pptx`.

Examples

Minimal

A support chat: outline, pill-shaped, attachments and Send only.

Limits

maxLength and tokenLimit with a counter that warns, then blocks.

Uploading

Progress rings, a failed upload with Retry, controlled attachments.

Streaming

Send becomes Stop; Escape stops too.

Phone, fixed

Pinned to the bottom above the keyboard, with suggestion chips above it.

Brand

tone="brand" on the send button and the active tool chips.

Sizes and variants

sm soft, md elevated, lg outline.

Dependencies

motionlucide-reacttextareabuttontoggletooltipdropdown-menuprogressspinnerfile-uploadfile-typesmodel-pickervoice-inputai-chatuse-keyboard-insetuse-controllable-stateuse-media-queryuse-scroll-edgesuse-shortcutuse-localenumber-formathapticsmotiontonesui-config

Files

  • components/ui/prompt-input.tsx

FAQ

Is the React Prompt Input component free?

The preview, props and docs are free; copying the source needs a Pro plan. See plans.

How do I install Prompt Input?

Run npx wingo-ui@latest add prompt-input. It also adds the Wingo UI items it builds on (Textarea, Button, Toggle, Tooltip, Dropdown Menu and 18 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/prompt-input.

How does Prompt Input behave on phones?

On a phone Enter always makes a new line and the round button sends without closing the keyboard.

See also

TextareaModel PickerVoice InputFile UploaduseKeyboardInsethook

Used in

Chat WidgetAI Chat Page

More React AI components

Reasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterFluid Orb
All 17 React AI components

Articles using this component

  • AI Elements Alternative: 5 React AI Chat Kits Compared

    Building UI with AI agents · Oct 16, 2026 · 11 min read

  • React Prompt Input: Enter, IME, Files, Stop and Voice

    Building UI with AI agents · Oct 16, 2026 · 10 min read

  • React Speech to Text Input: Mic, Waveform, Fallback

    Building UI with AI agents · Oct 16, 2026 · 9 min read

  • iOS Safari Keyboard Position Fixed: Keep a Composer Visible

    Mobile UI in React · Oct 13, 2026 · 9 min read

PreviousModel PickerNext Reasoning Indicator
Acme assistantOverdue invoices, September
  1. Which invoices are overdue from September? Make me a table and suggest what to do next.

  2. I found 3 overdue invoices from September, $22,775.95 in total. Should I draft the emails?

Shift + Enter for a new line
onSubmit: nothing sent yet
status="idle" · showCount="auto" · focusShortcut="" · stopShortcut="" · name="" · locale="" · accept="image/*,application/pdf,.doc,.docx,.xls,…" · maxFiles=10 · camera=true · safeArea=true · autoFocus=false