Mention menu

Type @ to pull a file, person or doc into the message as an inline pill, grouped by kind.

Loading preview…

Install

  1. 1

    Run this command in your Tailwind CSS project. It adds components/ai/mention-menu.tsx and installs its dependencies.

    $ npx shadcn@latest add https://gradientdeck.com/ai-components/r/mention-menu.json

Usage & API

Example

mention-menu.tsx
import { MentionMenu } from "@/components/ai/mention-menu"

<MentionMenu
  options={[]}
  segments={[]}
  onSegmentsChange={() => {}}
  draft="…"
  onDraftChange={() => {}}
  onSubmit={() => {}}
/>

Props MentionMenu

PropType & descriptionRequired
optionsMention[]Required
segmentsSegment[]Required
onSegmentsChange(segments: Segment[]) => voidRequired
draftstringRequired
onDraftChange(draft: string) => voidRequired
onSubmit(segments: Segment[]) => voidRequired

Dependencies

  • React
  • Tailwind CSS
  • motion
  • lucide-react
Implementation notes

Default exports MentionMenu. The panel is anchored above the card, full width of the inset on small screens and 320 pixels wide from sm up. Group labels are Files, People and Docs, in small caps. Each option shows the icon or initials, the label, and the detail truncated on the right. The active row is a moving highlight. Mouse down prevents blur before the pick. The field placeholder, when there are no segments, is Ask about a file, person or doc. Type @ to mention. It is blank once pills exist, so the placeholder does not sit under them. Send message is disabled until the draft has trimmed text or there is at least one segment. The demo announces Sent with mentions attached on a polite live region, then clears both segments and the draft. That reset is the demo, not the component. The combobox wires aria-expanded, aria-controls and aria-activedescendant the same way the slash menu does.

Choosing a layout

Use Mention menu when the model should receive structured references, not a string it has to guess at. onSubmit hands you the segments, and if the draft still has trimmed text it is concatenated as a final text segment. Your API can map each mention id to a file path, a user or a document. The demo starts from a sentence about pricing.tsx and leaves the draft on @, with options such as checkout.ts, Maya Okafor and Pricing review notes. Detail is the secondary column: a path, a team, or Edited 2d ago.

Use Slash command menu when the choice is a mode chip in front of the message, one command for the whole send, rather than several names inside the sentence. Use Prompt input when an @ should stay as characters. The workspace placeholder mentions @ files, but it will not open this list. Do not try to store the pills by scraping the input value. The input only holds the draft. The pills are segments, and the input's width shrinks to the draft once any segment exists, measured in ch.

How do I put the mentions back into a prompt string for the model?

Walk the segments you receive in onSubmit. Text segments contribute their text. Mention segments should contribute whatever your backend understands, usually the label or the id, because the visible pill is not in the input's value. The draft is already folded into that array when it has non-whitespace characters. If you need the kind, it is on the mention as file, person or doc. The component never joins them for you.

Backspace deletes the wrong thing when the caret is in the middle of the draft.

The pill-pop only runs when selectionStart and selectionEnd are both 0 and segments is non-empty. Anywhere else, Backspace edits the draft as a normal input. Arrow keys do not move into a pill. To remove a specific earlier pill you would change segments yourself. There is no per-pill remove button. The only removal gesture is that backspace at the boundary, which takes the last segment only.