Prompt input
The box people type into. Grows with the text, sends on Enter, and turns into a stop button while a reply streams.
Loading preview…
Install
- 1
Run this command in your Tailwind CSS project. It adds
components/ai/prompt-input-card.tsxand installs its dependencies.$ npx shadcn@latest add https://gradientdeck.com/ai-components/r/prompt-input-card.json
Build a streaming chat interface in React →
Usage & API
Example
import { PromptInputCard } from "@/components/ai/prompt-input-card"
<PromptInputCard
value="…"
onChange={() => {}}
onSubmit={() => {}}
/>Props PromptInputCard
| Prop | Type & description | Required |
|---|---|---|
value | string | Required |
onChange | (value: string) => void | Required |
onSubmit | (value: string) => void | Required |
onStop | () => void | Optional |
streaming | boolean | Optional |
placeholder | string | Optional |
model | string | Optional |
maxRows | number | Optional |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Card with toolbar is PromptInputCard. The placeholder defaults to Ask anything, the textarea is labelled Message, and clicking the card focuses it. Send message stays visible whenever streaming is false, and it is disabled while the trim is empty. Stop generating replaces it only while streaming is true, with a filled square. Search is a local toggle. The model string sits in the toolbar and is not a menu. maxRows limits the auto-grow. The send key is a warm square, distinct from the dark stop key.
Example
import { PromptInputPill } from "@/components/ai/prompt-input-pill"
<PromptInputPill
value="…"
onChange={() => {}}
onSubmit={() => {}}
/>Props PromptInputPill
| Prop | Type & description | Required |
|---|---|---|
value | string | Required |
onChange | (value: string) => void | Required |
onSubmit | (value: string) => void | Required |
onStop | () => void | Optional |
streaming | boolean | Optional |
placeholder | string | Optional |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Pill is PromptInputPill. One line stays a capsule because the form animates border radius between 999 and 24. The right-hand control is exclusive. Dictate shows when the field is empty and nothing is streaming. Send message shows once value.trim() is non-empty. Stop generating shows while streaming is true and calls onStop. Add attachment is a plus button with no handler. The placeholder defaults to Message. A spring layout transition runs as the box changes height, so the corners do not pop.
Example
import { PromptInputHero } from "@/components/ai/prompt-input-hero"
<PromptInputHero
value="…"
onChange={() => {}}
onSubmit={() => {}}
/>Props PromptInputHero
| Prop | Type & description | Required |
|---|---|---|
value | string | Required |
onChange | (value: string) => void | Required |
onSubmit | (value: string) => void | Required |
placeholder | string | Optional |
suggestions | string[] | Optional |
busy | boolean | Optional |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Hero is PromptInputHero. The placeholder defaults to Describe the app you want to build, and the textarea's accessible name is Describe what to build. busy replaces the word Build with Building and a pulsing sparkle, but there is no onStop and no streaming prop, so a long job does not turn into a stop button. The submit stays disabled until there is text and busy is false. A conic border of blue, indigo, pink and sand rotates over six seconds while the field is focused or busy, and it holds still when useReducedMotion is set. A soft radial wash behind the card brightens in the same cases. suggestions is a string array. Each one calls onChange with that string and focuses the textarea. The demo uses Habit tracker, Recipe finder, Portfolio with a blog and Invoice generator. Add image is visible and unwired.
Example
import { PromptInputDock } from "@/components/ai/prompt-input-dock"
<PromptInputDock
value="…"
onChange={() => {}}
onSubmit={() => {}}
mode={mode}
onModeChange={() => {}}
/>Props PromptInputDock
| Prop | Type & description | Required |
|---|---|---|
value | string | Required |
onChange | (value: string) => void | Required |
onSubmit | (value: string, mode: DockMode) => void | Required |
onStop | () => void | Optional |
streaming | boolean | Optional |
mode | DockMode | Required |
onModeChange | (mode: DockMode) => void | Required |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Floating dock is PromptInputDock. mode is ask, agent or plan, and onModeChange plus onSubmit(value, mode) are required. The radio group is labelled Mode. The three hints, used as placeholders, are Ask a question, Tell the agent what to do, and Describe the change to plan. The field is an input labelled Message, fixed at one line. Mention a file is hidden below the sm breakpoint and has no handler. Stop replaces Send while streaming. Send is disabled when the trim is empty. The active mode pill slides with a layout animation.
Example
import { PromptInputWorkspace } from "@/components/ai/prompt-input-workspace"
<PromptInputWorkspace
value="…"
onChange={() => {}}
onSubmit={() => {}}
models={[]}
model="…"
onModelChange={() => {}}
effort={effort}
onEffortChange={() => {}}
access={access}
onAccessChange={() => {}}
/>Props PromptInputWorkspace
| Prop | Type & description | Required |
|---|---|---|
project | string | Optional |
value | string | Required |
onChange | (value: string) => void | Required |
onSubmit | (value: string) => void | Required |
onStop | () => void | Optional |
onLeaveProject | () => void | Optional |
streaming | boolean | Optional |
models | string[] | Required |
model | string | Required |
onModelChange | (model: string) => void | Required |
effort | Effort | Required |
onEffortChange | (effort: Effort) => void | Required |
access | Access | Required |
onAccessChange | (access: Access) => void | Required |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Workspace is PromptInputWorkspace. If project is set, the heading reads What should we build in that name, and onLeaveProject renders or start without a project. With no project, the heading is What should we build. The placeholder mentions @ files, $ skills and / commands, but this variant does not parse those characters. They are a hint, not Mention menu or Slash command menu. The model button opens a menu labelled with a Model heading and a Reasoning effort radio group. Escape or a pointer outside closes it. Each model row is a menuitemradio. Attach files is unwired. The footer switch is access, full or ask, labelled Full access or Ask before changes, with aria-checked when access is full. Stop and Send swap on streaming. The demo models are Opus 5.5, Sonnet 5.5 and Haiku 5.5.
Example
import { PromptInputContextBar } from "@/components/ai/prompt-input-context-bar"
<PromptInputContextBar
value="…"
onChange={() => {}}
onSubmit={() => {}}
target={target}
onTargetChange={() => {}}
fullAccess={true}
onFullAccessChange={() => {}}
effort={effort}
onEffortChange={() => {}}
/>Props PromptInputContextBar
| Prop | Type & description | Required |
|---|---|---|
value | string | Required |
onChange | (value: string) => void | Required |
onSubmit | (value: string) => void | Required |
onStop | () => void | Optional |
onVoice | () => void | Optional |
streaming | boolean | Optional |
target | RunTarget | Required |
onTargetChange | (target: RunTarget) => void | Required |
fullAccess | boolean | Required |
onFullAccessChange | (on: boolean) => void | Required |
model | string | Optional |
effort | Effort | Required |
onEffortChange | (effort: Effort) => void | Required |
placeholder | string | Optional |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Context bar is PromptInputContextBar. A radio group labelled Run in sits on a tab tucked behind the top edge, with Choose project and This computer, backed by target and onTargetChange. The field placeholder defaults to Do anything. fullAccess is a switch labelled Full access or Ask first. Its title says the agent can change files without asking, or that it asks first, and the full-access state uses an orange alert icon. The model button does not open a list. It cycles effort through Low, Medium and High by calling onEffortChange, and its accessible name includes the model, the effort and Change effort. The default model string is Opus 5.5. Dictate is a separate mic, hidden on small screens, with no handler. The round button is Stop while streaming, Send when there is text, and Start voice mode when the field is empty, which calls onVoice.
Choosing a layout
Use Prompt input for a conversation, where the next action is a message and the button has to become a stop control while tokens arrive. Card with toolbar is the everyday thread. Pill fits a narrow column or a mobile sheet. Hero fits the first screen of a builder, with suggestion strings that write themselves into the field and focus it. Floating dock fits a bar that stays on screen, because Ask, Agent and Plan travel out through onSubmit as a second argument. Workspace fits an empty project, with a heading and a leave link. Context bar fits an agent that must say where it runs before it sends.
Do not use it for image generation. Image prompt composer is the box that carries aspect ratio, a style, a count, a leave-out line and reference roles. Do not treat the card's model label as Model picker. That button never opens a list. Workspace has a real menu, with the models array, a check on the current row, and a Reasoning effort radio group of Low, Medium and High. Slash command menu and Mention menu are separate composers if / or @ has to filter a list and leave a chip. Suggestion chips are the prompts above an empty thread, not the field itself.
Why did Enter send while I was still confirming a character?
It should not. Each growing layout checks event.nativeEvent.isComposing and returns before onSubmit. Shift+Enter also returns without submitting, so a newline is possible. Floating dock is the exception: it is a single-line input with no key handler, so Enter only submits because it is inside a form. If a compose event is still firing, check that you did not wrap the textarea in another onKeyDown that calls submit first.
The paperclip and the model name do nothing. Is that a bug?
On Card with toolbar, Pill and the dock's mention button, those controls are visual. Search on the card toggles a local pressed style and never leaves the component. Pass model only to change the label. Wire a real list with the workspace menu, or put Model picker next to the field. Context bar's effort button is wired, but only as a cycle of Low, Medium and High, not as a catalog of models.