AI suggestion
AIProAI inside forms: a suggestion row under a field, ghost text that completes what someone types, and an AiFill button that writes a field with a shimmer while it works.
npx prodular-ui add ai-suggestionPart of Prodular UI Pro. Preview and read the docs freely, install with a license. See pricing
In a form
A suggested category to accept with Tab or dismiss, and a business purpose written on request. Both wait for the person.
"use client"
import * as React from "react"
import { AiMark } from "@/components/ui/ai-mark"import { AiFill, AiFillField, AiSuggestion, useAiSuggestionKeys,} from "@/components/ui/ai-suggestion"import { Button } from "@/components/ui/button"import { Input } from "@/components/ui/input"import { Textarea } from "@/components/ui/textarea"
const generated = "Monthly hosting for the customer portal and nightly ledger exports. October usage was higher because of the month end close."
export default function AiSuggestionExpenseForm() { const [category, setCategory] = React.useState("") const [suggesting, setSuggesting] = React.useState(true) const [accepted, setAccepted] = React.useState(false) const [memo, setMemo] = React.useState("") const [generating, setGenerating] = React.useState(false) const [filled, setFilled] = React.useState(false)
const accept = React.useCallback(() => { setCategory("Cloud infrastructure") setSuggesting(false) setAccepted(true) }, []) const dismiss = React.useCallback(() => setSuggesting(false), []) const onKeyDown = useAiSuggestionKeys({ active: suggesting, onAccept: accept, onDismiss: dismiss })
const fill = () => { setGenerating(true) setTimeout(() => { setMemo(generated) setGenerating(false) setFilled(true) }, 1800) }
return ( <form className="flex w-full max-w-md flex-col gap-5 rounded-xl border bg-card p-5 shadow-xs" onSubmit={(e) => e.preventDefault()} > <div className="flex items-baseline justify-between"> <h3 className="text-sm font-medium">New expense</h3> <span className="text-[13px] text-muted-foreground tabular-nums">$1,284.50 · Oct 18</span> </div>
<div className="flex flex-col gap-1.5"> <label htmlFor="merchant" className="text-sm font-medium"> Merchant </label> <Input id="merchant" defaultValue="Lumen Cloud Services" /> </div>
<div className="flex flex-col gap-1.5"> <label htmlFor="category" className="text-sm font-medium"> Category </label> <Input id="category" value={category} placeholder="Choose or type a category" onChange={(e) => { setCategory(e.target.value) setAccepted(false) }} onKeyDown={onKeyDown} /> {suggesting && ( <AiSuggestion label="Suggested" reason="Used for this merchant 9 times" onAccept={accept} onDismiss={dismiss} > Cloud infrastructure </AiSuggestion> )} {accepted && ( <p className="text-[13px] text-muted-foreground"> Accepted the suggestion.{" "} <button type="button" className="text-foreground underline-offset-4 hover:underline" onClick={() => { setCategory("") setAccepted(false) setSuggesting(true) }} > Undo </button> </p> )} </div>
<div className="flex flex-col gap-1.5"> <div className="flex items-center justify-between"> <label htmlFor="memo" className="text-sm font-medium"> Business purpose </label> <AiFill generating={generating} label="Write for me" onClick={fill} /> </div> <AiFillField generating={generating} status="Reading the receipt and last month's memo" align="start"> <Textarea id="memo" value={memo} readOnly={generating} onChange={(e) => { setMemo(e.target.value) setFilled(false) }} placeholder="Why the company paid for this" className="min-h-20" /> </AiFillField> {filled && ( <p className="flex items-center gap-1.5 text-[13px] text-muted-foreground"> <AiMark variant="inline" label="Written by AI" /> <span>Check it before you submit.</span> </p> )} </div>
<div className="flex gap-2"> <Button type="submit">Submit expense</Button> <Button type="button" variant="ghost"> Save draft </Button> </div> </form> )}Ghost text
The rest of the suggested value appears after the caret while it still matches what was typed.
Press Tab to accept the suggestion, Escape to dismiss it. Try typing Request or Reset.
"use client"
import * as React from "react"
import { AiGhostInput } from "@/components/ui/ai-suggestion"
const suggestions = [ "Refund for duplicate charge on the October invoice", "Request to add 5 seats to the Team plan", "Reset two factor authentication for an admin",]
export default function AiSuggestionGhostText() { const [value, setValue] = React.useState("Refund for dup") const [dismissed, setDismissed] = React.useState(false)
const suggestion = dismissed ? null : (suggestions.find((s) => value.length > 2 && s.toLowerCase().startsWith(value.toLowerCase())) ?? null)
return ( <div className="flex w-full max-w-md flex-col gap-1.5"> <label htmlFor="ticket-subject" className="text-sm font-medium"> Ticket subject </label> <AiGhostInput id="ticket-subject" value={value} onValueChange={(v) => { setValue(v) setDismissed(false) }} suggestion={suggestion === value ? null : suggestion} onDismiss={() => setDismissed(true)} placeholder="Summarize the request" /> <p className="text-[13px] text-muted-foreground"> Press Tab to accept the suggestion, Escape to dismiss it. Try typing Request or Reset. </p> </div> )}Anatomy
<Input value={category} onKeyDown={onKeyDown} /><AiSuggestion label="Suggested" reason="Used for this merchant 9 times" onAccept={accept} onDismiss={dismiss}> Cloud infrastructure</AiSuggestion>Guidance
Use it when
- Use AiSuggestion for a single value the model is fairly sure about: a category, an assignee, a GL account. Say why in reason.
- Use AiGhostInput for short free text where completion saves typing: subjects, titles, names.
- Use AiFill next to a field label for longer text the person asked for: a memo, a description, a reply.
- After an AI fill, mark the field with AiMark inline and keep it editable.
Avoid
- Filling fields without being asked. Suggestions are offered, never applied.
- More than one suggestion row per field, or suggestions on every field of a form. Pick the fields where the model helps most.
- Ghost text in multi line fields or in fields with strict formats like amounts and dates.
Rules for AI agents
Published in llms-full.txt, the registry and the MCP server, so coding agents follow them when they build with this component.
- Never auto accept an AiSuggestion or ghost text. Apply the value only in onAccept, which fires on the Accept button or Tab.
- Wire Tab and Escape on the field with useAiSuggestionKeys({ active, onAccept, onDismiss }). Only set active while the suggestion is visible, so Tab keeps moving focus otherwise.
- Hide the suggestion once it is accepted, dismissed, or the person types a different value. Offer Undo after accepting.
- While AiFill runs, pass generating to both AiFill and AiFillField and make the control readOnly. Do not let the person submit the form with a half written value.
- After AiFill completes, show AiMark inline next to the field until the person edits the text.
API
AiSuggestion
| Prop | Type | Default | Description |
|---|---|---|---|
| label / children / reason | ReactNode | What is suggested, the suggested value and why. | |
| onAccept | () => void | Required. Apply the value here and nowhere else. | |
| onDismiss | () => void | Shows the dismiss button. | |
| shortcut | string | null | "Tab" | Key hint on the accept button. |
AiGhostInput
| Prop | Type | Default | Description |
|---|---|---|---|
| value / onValueChange | string / (value: string) => void | Controlled input value. | |
| suggestion | string | null | The full suggested value. Ghost text shows while it starts with value. |
AiFill
| Prop | Type | Default | Description |
|---|---|---|---|
| generating | boolean | false | Animated mark, shimmer label and aria-busy. |
| iconOnly | boolean | false | For input addons. label stays the accessible name. |
AiFillField
| Prop | Type | Default | Description |
|---|---|---|---|
| generating / status / align | boolean / ReactNode / "center" | "start" | Overlay while the field is written. start for textareas. |
useAiSuggestionKeys
| Prop | Type | Default | Description |
|---|---|---|---|
| options | { active, onAccept, onDismiss } | Returns an onKeyDown handler for the field. |
Installation
The Prodular UI CLI adds the source to components/ui with everything it depends on. In a project that already uses shadcn/ui, the shadcn CLI installs it from the Prodular registry too. See Installation for the first setup.
npx prodular-ui add ai-suggestion# or, with the registry in components.jsonnpx shadcn@latest add @prodular/ai-suggestion