Ghost completion
Grey suggested text that appears after a pause in typing. Tab takes it, Esc dismisses it.
Loading preview…
Install
- 1
Run this command in your Tailwind CSS project. It adds
components/ai/ghost-completion.tsxand installs its dependencies.$ npx shadcn@latest add https://gradientdeck.com/ai-components/r/ghost-completion.json
Usage & API
Example
import { GhostCompletion } from "@/components/ai/ghost-completion"
<GhostCompletion
value="…"
onChange={() => {}}
getSuggestion={() => {}}
/>Props GhostCompletion
| Prop | Type & description | Required |
|---|---|---|
value | string | Required |
onChange | (value: string) => void | Required |
getSuggestion | (value: string) => string | nullReturn the text to suggest after the current value, or null for nothing. | Required |
delay | numberMilliseconds of quiet before a suggestion appears. | Optional |
placeholder | string | Optional |
suggestionOverride | string | nullLets a parent drive the editor (used by the demo). | Optional |
onAccept | (accepted: string) => void | Optional |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Default is the single field, a fixed-height box with the mirror, the textarea, and the footer. The mirror is aria-hidden. The textarea points at the footer as its description. Scroll on the textarea copies scrollTop to the mirror so a long draft does not let the grey text drift. Accept writes value plus the suggestion and places the caret at the end on the next frame. Escape only clears the internally fetched suggestion. If a parent is passing suggestionOverride, that string is what remains visible, which is how the preview can hold a line on screen during its script. The demo script types Hi Maya and a thank-you line, shows a continuation about reading the numbers, then accepts it and releases the override. Reduced motion skips the fade on the grey text. There is no button to accept. The keys are the control.
Choosing a layout
Use it inside a draft the person is actually writing, where the model can offer the rest of a sentence without taking the cursor away. Mail, notes, and short posts fit the single field. The accept gesture is Tab, which is easy to hit and easy to ignore: if they keep typing, the grey text is gone. That is the right contract for an optional continuation. Put the footer where they can learn the keys once. The hints hide on a very narrow width, but Tab and Escape still work, and the chip on the suggestion remains.
Do not use it when the person asked for a specific edit. Inline prompt is the bar for one instruction about a paragraph, with a discard and an accept after the rewrite. Selection toolbar is the menu on a highlight: Improve, Shorten, Translate, or Ask. Prose diff is the marked-up result when each change should be accepted alone. Ghost text has none of those actions. It also refuses to suggest unless the caret is at the end, so it is the wrong UI for an edit in the middle of a sentence. Version history is the later step, once drafts exist and someone needs an older one back.
Why did a suggestion not appear?
The timer starts only for a non-empty value with the caret at the very end, and only when suggestionOverride is undefined. A selection, a caret in the middle, or a blank field clears it. getSuggestion can also return null. The footer stays on Pause to get a suggestion in that last case, because nothing was shown. The default pause is 650 milliseconds, and delay changes it.
What does Tab insert?
The suggestion string currently shown, appended to value with no extra trimming inside the component. On accept, onAccept gets that same string, and the local suggestion clears. Shift+Tab is left alone. Escape dismisses instead of inserting. Typing any other character goes through onChange and the suggestion is thrown away.